Feroze Mohideen 2 éve
szülő
commit
b5166aa788
100 módosított fájl, 0 hozzáadás és 25280 törlés
  1. 0 71
      dashboard/src/legacy/main/home/app-dashboard/apps/SelectableAppList.tsx
  2. 0 36
      dashboard/src/legacy/main/home/app-dashboard/types/buildpack.ts
  3. 0 47
      dashboard/src/legacy/main/home/cluster-dashboard/preview-environments/v2/DeleteEnvModal.tsx
  4. 0 205
      dashboard/src/legacy/main/home/managed-addons/tabs/PostgresTabs.tsx
  5. 0 195
      dashboard/src/legacy/main/home/managed-addons/tabs/RedisTabs.tsx
  6. 0 34
      dashboard/src/legacy/main/home/managed-addons/tabs/shared.tsx
  7. 0 110
      dashboard/src/legacy/main/home/onboarding/components/RegistryImageList.tsx
  8. 0 63
      dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ConditionsTable.tsx
  9. 0 200
      dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ExpandedNodeView.tsx
  10. 0 134
      dashboard/src/main/home/cluster-dashboard/dashboard/node-view/NodeUsage.tsx
  11. 0 819
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx
  12. 0 70
      dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx
  13. 0 163
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx
  14. 0 194
      dashboard/src/main/home/cluster-dashboard/expanded-chart/NotificationSettingsSection.tsx
  15. 0 558
      dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx
  16. 0 491
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  17. 0 349
      dashboard/src/main/home/cluster-dashboard/expanded-chart/TagSelector.tsx
  18. 0 151
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx
  19. 0 598
      dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx
  20. 0 554
      dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/_BuildpackConfigSection.tsx
  21. 0 29
      dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/types.ts
  22. 0 391
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx
  23. 0 219
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx
  24. 0 149
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx
  25. 0 223
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodRow.tsx
  26. 0 316
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ResourceTab.tsx
  27. 0 66
      dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts
  28. 0 697
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx
  29. 0 265
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx
  30. 0 155
      dashboard/src/main/home/cluster-dashboard/expanded-chart/events/styles.ts
  31. 0 100
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Edge.tsx
  32. 0 750
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx
  33. 0 245
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx
  34. 0 135
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx
  35. 0 60
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/SelectRegion.tsx
  36. 0 91
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/ZoomPanel.tsx
  37. 0 9
      dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/DisabledNamespaces.ts
  38. 0 61
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ConnectToJobInstructionsModal.tsx
  39. 0 555
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx
  40. 0 208
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx
  41. 0 342
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx
  42. 0 397
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/useJobs.ts
  43. 0 662
      dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx
  44. 0 436
      dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs.ts
  45. 0 497
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx
  46. 0 561
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/JobMetricsSection.tsx
  47. 0 174
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts
  48. 0 750
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx
  49. 0 439
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  50. 0 398
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx
  51. 0 234
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/PodRow.tsx
  52. 0 288
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  53. 0 19
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/types.ts
  54. 0 218
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/useLogs.ts
  55. 0 79
      dashboard/src/main/home/cluster-dashboard/expanded-chart/useStackEnvGroups.ts
  56. 0 226
      dashboard/src/main/home/cluster-dashboard/jobs/JobDashboard.tsx
  57. 0 617
      dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx
  58. 0 116
      dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepoActionConfEditor.tsx
  59. 0 58
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/ActionButton.tsx
  60. 0 97
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx
  61. 0 173
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx
  62. 0 164
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/NamespaceLabels.tsx
  63. 0 98
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/PorterYAMLErrorsModal.tsx
  64. 0 104
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx
  65. 0 84
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/styled.tsx
  66. 0 674
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx
  67. 0 767
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx
  68. 0 602
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentList.tsx
  69. 0 76
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PreviewEnvDeleted.tsx
  70. 0 538
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx
  71. 0 115
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx
  72. 0 591
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreatePREnvironment.tsx
  73. 0 305
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx
  74. 0 493
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentSettings.tsx
  75. 0 184
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx
  76. 0 3
      dashboard/src/main/home/cluster-dashboard/preview-environments/errors.ts
  77. 0 177
      dashboard/src/main/home/cluster-dashboard/preview-environments/mocks.ts
  78. 0 51
      dashboard/src/main/home/cluster-dashboard/preview-environments/routes.tsx
  79. 0 50
      dashboard/src/main/home/cluster-dashboard/preview-environments/utils.ts
  80. 0 156
      dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx
  81. 0 358
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx
  82. 0 160
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_Settings.tsx
  83. 0 162
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_TemplateSelector.tsx
  84. 0 27
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/index.tsx
  85. 0 66
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewEnvGroup.tsx
  86. 0 99
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/Store.tsx
  87. 0 320
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_RevisionList.tsx
  88. 0 222
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx
  89. 0 116
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/EnvGroups.tsx
  90. 0 259
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx
  91. 0 139
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx
  92. 0 328
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/SourceEditorDocker.tsx
  93. 0 63
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/routes.tsx
  94. 0 264
      dashboard/src/main/home/cluster-dashboard/stacks/_StackList.tsx
  95. 0 316
      dashboard/src/main/home/cluster-dashboard/stacks/components/NewAppResourceForm.tsx
  96. 0 165
      dashboard/src/main/home/cluster-dashboard/stacks/components/NewEnvGroupForm.tsx
  97. 0 25
      dashboard/src/main/home/cluster-dashboard/stacks/components/Status.tsx
  98. 0 67
      dashboard/src/main/home/cluster-dashboard/stacks/launch/NewApp.tsx
  99. 0 30
      dashboard/src/main/home/cluster-dashboard/stacks/launch/NewEnvGroup.tsx
  100. 0 315
      dashboard/src/main/home/cluster-dashboard/stacks/launch/Overview.tsx

+ 0 - 71
dashboard/src/legacy/main/home/app-dashboard/apps/SelectableAppList.tsx

@@ -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<AppListProps> = ({ appListItems }) => {
-  return (
-    <SelectableList
-      selectedIcon={healthy}
-      listItems={appListItems.map((ali) => {
-        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: (
-            <>
-              <Container row>
-                <Spacer inline width="1px" />
-                <AppIcon
-                  buildpacks={proto.build?.buildpacks ?? []}
-                  size="larger"
-                />
-                <Spacer inline width="12px" />
-                <Text size={14}>{proto.name}</Text>
-                <Spacer inline x={1} />
-              </Container>
-              <Spacer height="15px" />
-              <Container row>
-                <AppSource
-                  source={{
-                    from: "porter_apps",
-                    details: ali.app.source,
-                  }}
-                />
-                <Spacer inline x={1} />
-              </Container>
-            </>
-          ),
-          key: ali.key,
-          onSelect: ali.onSelect,
-          onDeselect: ali.onDeselect,
-          isSelected: ali.isSelected,
-        };
-      })}
-    />
-  );
-};
-
-export default SelectableAppList;

+ 0 - 36
dashboard/src/legacy/main/home/app-dashboard/types/buildpack.ts

@@ -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<typeof buildConfigSchema>;
-
-export const buildpackSchema = z.object({
-  name: z.string(),
-  buildpack: z.string(),
-  config: z.record(z.any()).nullish(),
-});
-export type Buildpack = z.infer<typeof buildpackSchema>;
-
-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<typeof detectedBuildpackSchema>;
-
-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",
-};

+ 0 - 47
dashboard/src/legacy/main/home/cluster-dashboard/preview-environments/v2/DeleteEnvModal.tsx

@@ -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<void>;
-  loading?: boolean;
-};
-
-const DeleteEnvModal: React.FC<Props> = ({
-  closeModal,
-  deleteEnv,
-  loading = false,
-}) => {
-  return (
-    <Modal closeModal={closeModal}>
-      <Text size={16}>Confirm deletion</Text>
-      <Spacer y={0.5} />
-      <Text color="helper">
-        Click the button below to confirm environment deletion. This action is
-        irreversible.
-      </Text>
-      <Spacer y={0.5} />
-      <Text color="helper">
-        Deleting this environment will tear down all apps and any associated
-        resources.
-      </Text>
-      <Spacer y={1} />
-      <Button
-        onClick={() => {
-          void deleteEnv();
-        }}
-        color="#b91133"
-        status={loading ? "loading" : ""}
-        loadingText="Deleting..."
-        disabled={loading}
-      >
-        Delete
-      </Button>
-    </Modal>
-  );
-};
-
-export default DeleteEnvModal;

+ 0 - 205
dashboard/src/legacy/main/home/managed-addons/tabs/PostgresTabs.tsx

@@ -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<ClientAddon, "template"> & {
-    config: {
-      type: "postgres";
-    };
-  };
-};
-
-export const PostgresTabs: React.FC<Props> = ({ index }) => {
-  const { register, control, watch } = useFormContext<AppTemplateFormData>();
-  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 (
-    <>
-      <TabSelector
-        options={[
-          { label: "Credentials", value: "credentials" },
-          { label: "Resources", value: "resources" },
-        ]}
-        currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
-      />
-      <Spacer y={1} />
-      {match(currentTab)
-        .with("credentials", () => (
-          <>
-            <Text color="helper">Postgres Username</Text>
-            <Spacer y={0.25} />
-            <ControlledInput
-              type="text"
-              placeholder="postgres"
-              width="300px"
-              {...register(`addons.${index}.config.username`)}
-            />
-            <Spacer y={1} />
-            <Text color="helper">Postgres Password</Text>
-            <Spacer y={0.25} />
-            <ControlledInput
-              type="text"
-              width="300px"
-              {...register(`addons.${index}.config.password`)}
-            />
-            <Spacer y={1} />
-            {databaseURL && (
-              <>
-                <Text color="helper">Internal Database URL:</Text>
-                <Spacer y={0.5} />
-                <IdContainer>
-                  <Code>{databaseURL}</Code>
-                  <CopyContainer>
-                    <CopyToClipboard text={databaseURL}>
-                      <CopyIcon src={copy} alt="copy" />
-                    </CopyToClipboard>
-                  </CopyContainer>
-                </IdContainer>
-                <Spacer y={0.5} />
-              </>
-            )}
-          </>
-        ))
-        .with("resources", () => (
-          <>
-            <Controller
-              name={`addons.${index}.config.cpuCores`}
-              control={control}
-              render={({ field: { value, onChange } }) =>
-                match(value)
-                  .with(P.number, (v) => (
-                    <IntelligentSlider
-                      label="CPUs: "
-                      unit="Cores"
-                      min={0.01}
-                      max={maxCpuCores}
-                      color={"#3f51b5"}
-                      value={v.toString()}
-                      setValue={(e) => {
-                        onChange(e);
-                      }}
-                      step={0.1}
-                      disabled={false}
-                      disabledTooltip={
-                        "You may only edit this field in your porter.yaml."
-                      }
-                      isSmartOptimizationOn={false}
-                      decimalsToRoundTo={2}
-                    />
-                  ))
-                  .otherwise((v) => (
-                    <IntelligentSlider
-                      label="CPUs: "
-                      unit="Cores"
-                      min={0.01}
-                      max={maxCpuCores}
-                      color={"#3f51b5"}
-                      value={v.value.toString()}
-                      setValue={(e) => {
-                        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}
-                    />
-                  ))
-              }
-            />
-            <Spacer y={1} />
-            <Controller
-              name={`addons.${index}.config.ramMegabytes`}
-              control={control}
-              render={({ field: { value, onChange } }) =>
-                match(value)
-                  .with(P.number, (v) => (
-                    <IntelligentSlider
-                      label="RAM: "
-                      unit="MB"
-                      min={1}
-                      max={maxRamMegabytes}
-                      color={"#3f51b5"}
-                      value={v.toString()}
-                      setValue={(e) => {
-                        onChange(e);
-                      }}
-                      step={10}
-                      disabled={false}
-                      disabledTooltip={
-                        "You may only edit this field in your porter.yaml."
-                      }
-                      isSmartOptimizationOn={false}
-                    />
-                  ))
-                  .otherwise((v) => (
-                    <IntelligentSlider
-                      label="RAM: "
-                      unit="MB"
-                      min={1}
-                      max={maxRamMegabytes}
-                      color={"#3f51b5"}
-                      value={v.value.toString()}
-                      setValue={(e) => {
-                        onChange({
-                          ...v,
-                          value: e,
-                        });
-                      }}
-                      step={10}
-                      disabled={v.readOnly}
-                      disabledTooltip={
-                        "You may only edit this field in your porter.yaml."
-                      }
-                      isSmartOptimizationOn={false}
-                    />
-                  ))
-              }
-            />
-          </>
-        ))
-        .exhaustive()}
-    </>
-  );
-};

+ 0 - 195
dashboard/src/legacy/main/home/managed-addons/tabs/RedisTabs.tsx

@@ -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<ClientAddon, "template"> & {
-    config: {
-      type: "redis";
-    };
-  };
-};
-
-export const RedisTabs: React.FC<Props> = ({ index }) => {
-  const { register, control, watch } = useFormContext<AppTemplateFormData>();
-  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 (
-    <>
-      <TabSelector
-        options={[
-          { label: "Credentials", value: "credentials" },
-          { label: "Resources", value: "resources" },
-        ]}
-        currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
-      />
-      <Spacer y={1} />
-      {match(currentTab)
-        .with("credentials", () => (
-          <>
-            <Text color="helper">Redis Password</Text>
-            <Spacer y={0.25} />
-            <ControlledInput
-              type="text"
-              width="300px"
-              {...register(`addons.${index}.config.password`)}
-            />
-            <Spacer y={1} />
-            {redisURL && (
-              <>
-                <Text color="helper">Internal Redis URL:</Text>
-                <Spacer y={0.5} />
-                <IdContainer>
-                  <Code>{redisURL}</Code>
-                  <CopyContainer>
-                    <CopyToClipboard text={redisURL}>
-                      <CopyIcon src={copy} alt="copy" />
-                    </CopyToClipboard>
-                  </CopyContainer>
-                </IdContainer>
-                <Spacer y={0.5} />
-              </>
-            )}
-          </>
-        ))
-        .with("resources", () => (
-          <>
-            <Controller
-              name={`addons.${index}.config.cpuCores`}
-              control={control}
-              render={({ field: { value, onChange } }) =>
-                match(value)
-                  .with(P.number, (v) => (
-                    <IntelligentSlider
-                      label="CPUs: "
-                      unit="Cores"
-                      min={0.01}
-                      max={maxCpuCores}
-                      color={"#3f51b5"}
-                      value={v.toString()}
-                      setValue={(e) => {
-                        onChange(e);
-                      }}
-                      step={0.1}
-                      disabled={false}
-                      disabledTooltip={
-                        "You may only edit this field in your porter.yaml."
-                      }
-                      isSmartOptimizationOn={false}
-                      decimalsToRoundTo={2}
-                    />
-                  ))
-                  .otherwise((v) => (
-                    <IntelligentSlider
-                      label="CPUs: "
-                      unit="Cores"
-                      min={0.01}
-                      max={maxCpuCores}
-                      color={"#3f51b5"}
-                      value={v.value.toString()}
-                      setValue={(e) => {
-                        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}
-                    />
-                  ))
-              }
-            />
-            <Spacer y={1} />
-            <Controller
-              name={`addons.${index}.config.ramMegabytes`}
-              control={control}
-              render={({ field: { value, onChange } }) =>
-                match(value)
-                  .with(P.number, (v) => (
-                    <IntelligentSlider
-                      label="RAM: "
-                      unit="MB"
-                      min={1}
-                      max={maxRamMegabytes}
-                      color={"#3f51b5"}
-                      value={v.toString()}
-                      setValue={(e) => {
-                        onChange(e);
-                      }}
-                      step={10}
-                      disabled={false}
-                      disabledTooltip={
-                        "You may only edit this field in your porter.yaml."
-                      }
-                      isSmartOptimizationOn={false}
-                    />
-                  ))
-                  .otherwise((v) => (
-                    <IntelligentSlider
-                      label="RAM: "
-                      unit="MB"
-                      min={1}
-                      max={maxRamMegabytes}
-                      color={"#3f51b5"}
-                      value={v.value.toString()}
-                      setValue={(e) => {
-                        onChange({
-                          ...v,
-                          value: e,
-                        });
-                      }}
-                      step={10}
-                      disabled={v.readOnly}
-                      disabledTooltip={
-                        "You may only edit this field in your porter.yaml."
-                      }
-                      isSmartOptimizationOn={false}
-                    />
-                  ))
-              }
-            />
-          </>
-        ))
-        .exhaustive()}
-    </>
-  );
-};

+ 0 - 34
dashboard/src/legacy/main/home/managed-addons/tabs/shared.tsx

@@ -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;
-`;

+ 0 - 110
dashboard/src/legacy/main/home/onboarding/components/RegistryImageList.tsx

@@ -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(
-        "<token>",
-        {},
-        {
-          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 (
-    <>
-      <Helper>Porter was able to successfully connect to your registry:</Helper>
-      <ImageList>
-        {imageList.length > 0 ? (
-          imageList.map((data, i) => (
-            <ImageRow isLast={i === imageList.length - 1}>
-              <img src={getIcon()} />
-              {data.uri}
-            </ImageRow>
-          ))
-        ) : (
-          <Placeholder>No container images found.</Placeholder>
-        )}
-      </ImageList>
-      <Br />
-    </>
-  );
-};
-
-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;
-`;

+ 0 - 63
dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ConditionsTable.tsx

@@ -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<NodeStatusModalProps> = ({
-  node,
-}) => {
-  const columns = useMemo<Column<any>[]>(
-    () => [
-      {
-        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<Array<any>>(() => {
-    return node?.node_conditions || [];
-  }, [node]);
-
-  return (
-    <div>
-      <TableWrapper>
-        <Table
-          columns={columns}
-          data={data}
-          isLoading={!data.length}
-          disableGlobalFilter={true}
-        />
-      </TableWrapper>
-    </div>
-  );
-};
-
-const TableWrapper = styled.div`
-  margin-top: 36px;
-`;

+ 0 - 200
dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ExpandedNodeView.tsx

@@ -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<ExpandedNodeViewParams>();
-  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(
-        "<token>",
-        {},
-        {
-          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 <ConditionsTable node={node} />;
-    }
-  }, [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 (
-    <StyledExpandedNodeView>
-      <BreadcrumbRow>
-        <Breadcrumb onClick={closeNodeView}>
-          <ArrowIcon src={leftArrow} />
-          <Wrap>Back</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <HeaderWrapper>
-        <TitleSection icon={nodePng}>
-          {nodeId}
-          <InstanceType>{instanceType}</InstanceType>
-        </TitleSection>
-      </HeaderWrapper>
-      <BodyWrapper>
-        <NodeUsage node={node} />
-
-        <StatusWrapper>
-          <StatusSection status={nodeStatus} />
-        </StatusWrapper>
-
-        <TabSelector
-          options={tabOptions}
-          currentTab={currentTab}
-          setCurrentTab={(value: TabEnum) => setCurrentTab(value)}
-        />
-        {currentTabPage}
-      </BodyWrapper>
-    </StyledExpandedNodeView>
-  );
-};
-
-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;
-    }
-  }
-`;

+ 0 - 134
dashboard/src/main/home/cluster-dashboard/dashboard/node-view/NodeUsage.tsx

@@ -1,134 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-
-type NodeUsageProps = {
-  node: any;
-};
-
-const NodeUsage: React.FunctionComponent<NodeUsageProps> = ({ 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 (
-    <NodeUsageWrapper>
-      <Wrapper>
-        <UsageWrapper>
-          <span>
-            <Bolded>CPU:</Bolded>{" "}
-            {!node?.cpu_reqs && !node?.allocatable_cpu
-              ? "Loading..."
-              : `${percentFormatter(node?.fraction_cpu_reqs)} (${
-                  node?.cpu_reqs
-                }/${node?.allocatable_cpu}m)`}
-          </span>
-          <Buffer />
-          <span>
-            <Bolded>RAM:</Bolded>{" "}
-            {!node?.memory_reqs && !node?.allocatable_memory
-              ? "Loading..."
-              : `${percentFormatter(
-                  node?.fraction_memory_reqs
-                )} (${formatMemoryUnitToMi(
-                  node?.memory_reqs
-                )}/${formatMemoryUnitToMi(node?.allocatable_memory)})`}
-          </span>
-          <I
-            onClick={() =>
-              window.open(
-                "https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/#node-allocatable"
-              )
-            }
-            className="material-icons"
-          >
-            help_outline
-          </I>
-        </UsageWrapper>
-      </Wrapper>
-    </NodeUsageWrapper>
-  );
-};
-
-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;

+ 0 - 819
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -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<PropsType> = ({ 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 (
-        <Placeholder>
-          <TextWrap>
-            <Header>
-              <Spinner src={loading} /> This job is currently being deployed
-            </Header>
-            Navigate to the
-            <A
-              href={`https://github.com/${chart?.git_action_config?.git_repo}/actions`}
-              target={"_blank"}
-            >
-              Actions tab
-            </A>{" "}
-            of your GitHub repo to view live build logs.
-          </TextWrap>
-        </Placeholder>
-      );
-    }
-
-    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 (
-        <TabWrapper>
-          <ButtonWrapper
-            style={{
-              marginBottom: chart?.config?.schedule?.enabled ? "0px" : "35px",
-            }}
-          >
-            <SaveButton
-              onClick={() => {
-                runJob();
-              }}
-              status={triggerRunStatus}
-              makeFlush={true}
-              clearPosition={true}
-              rounded={true}
-              statusPosition="right"
-            >
-              <i className="material-icons">play_arrow</i> Run Job
-            </SaveButton>
-            <CLIModalIconWrapper
-              onClick={(e) => {
-                e.preventDefault();
-                setShowConnectionModal(true);
-              }}
-            >
-              <CLIModalIcon />
-              Shell Access
-            </CLIModalIconWrapper>
-          </ButtonWrapper>
-
-          {chart?.config?.schedule?.enabled ? (
-            <RunsDescription>
-              <i className="material-icons">access_time</i>
-              {runDescription}
-              <Dot
-                style={{
-                  color: "#ffffff88",
-                }}
-              >
-                •
-              </Dot>{" "}
-              Next run on
-              {" " + rtf.format(interval.next().toDate())}
-            </RunsDescription>
-          ) : null}
-
-          {jobsStatus === "loading" ? (
-            <Loading></Loading>
-          ) : (
-            <>
-              <JobList
-                jobs={jobs}
-                setJobs={() => { }}
-                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)}
-              />
-            </>
-          )}
-        </TabWrapper>
-      );
-    }
-
-    if (currentTab === "values") {
-      return (
-        <ValuesYaml
-          currentChart={chart}
-          refreshChart={() => refreshChart()}
-          disabled={!isAuthorized("job", "", ["get", "update"])}
-        />
-      );
-    }
-
-    if (currentTab === "build-settings") {
-      return (
-        <BuildSettingsTab
-          chart={chart}
-          isPreviousVersion={disableForm}
-          onSave={refreshChart}
-        />
-      );
-    }
-
-    if (
-      currentTab === "settings" &&
-      isAuthorized("job", "", ["get", "delete"])
-    ) {
-      return (
-        <SettingsSection
-          currentChart={chart}
-          refreshChart={() => 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 <Loading />;
-  }
-
-  if (status === "deleting") {
-    return (
-      <StyledExpandedChart>
-        <ExpandedJobHeader
-          chart={chart}
-          jobs={jobs}
-          disableRevisions
-          closeChart={closeChart}
-          refreshChart={refreshChart}
-          upgradeChart={upgradeChart}
-          loadChartWithSpecificRevision={loadChartWithSpecificRevision}
-          setDisableForm={setDisableForm}
-        />
-        <LineBreak />
-        <Placeholder>
-          <TextWrap>
-            <Header>
-              <Spinner src={loading} /> Deleting "{chart?.name}"
-            </Header>
-            You will be automatically redirected after deletion is complete.
-          </TextWrap>
-        </Placeholder>
-      </StyledExpandedChart>
-    );
-  }
-
-  if (selectedJob !== null) {
-    return (
-      <ExpandedJobRun
-        currentChart={chart}
-        jobRun={selectedJob}
-        onClose={() => {
-          const app = getQueryParam({ location }, "app");
-          if (app) {
-            window.location.href = `/apps/${app}`;
-          } else {
-            setSelectedJob(null);
-          }
-        }}
-      />
-    );
-  }
-
-  return (
-    <>
-      <ConnectToJobInstructionsModal
-        show={showConnectionModal}
-        onClose={() => setShowConnectionModal(false)}
-        chartName={chart?.name}
-      />
-      <StyledExpandedChart>
-        <ExpandedJobHeader
-          chart={chart}
-          jobs={jobs}
-          closeChart={closeChart}
-          refreshChart={refreshChart}
-          upgradeChart={upgradeChart}
-          loadChartWithSpecificRevision={loadChartWithSpecificRevision}
-          setDisableForm={setDisableForm}
-        />
-        <BodyWrapper>
-          {(leftTabOptions?.length > 0 ||
-            formData.tabs?.length > 0 ||
-            rightTabOptions?.length > 0) && (
-              <PorterFormWrapper
-                formData={formData}
-                valuesToOverride={{
-                  namespace: chart?.namespace,
-                  clusterId: currentCluster?.id,
-                }}
-                renderTabContents={renderTabContents}
-                isReadOnly={
-                  hasPorterImageTemplate ||
-                  !isAuthorized("job", "", ["get", "update"]) ||
-                  disableForm
-                }
-                onSubmit={(formValues) =>
-                  updateChart(processValuesToUpdateChart(formValues))
-                }
-                includeMetadata
-                leftTabOptions={leftTabOptions}
-                rightTabOptions={rightTabOptions}
-                saveValuesStatus={saveStatus}
-                saveButtonText="Save config"
-                includeHiddenFields
-                addendum={
-                  <TabButton
-                    onClick={() =>
-                      setDevOpsMode((prev) => {
-                        localStorage.setItem("devOpsMode", prev.toString());
-                        return !prev;
-                      })
-                    }
-                    devOpsMode={devOpsMode}
-                  >
-                    <i className="material-icons">offline_bolt</i> DevOps Mode
-                  </TabButton>
-                }
-                injectedProps={{
-                  "key-value-array": {
-                    availableSyncEnvGroups:
-                      isStack && !disableForm ? stackEnvGroups : undefined,
-                  },
-                  "url-link": {
-                    chart: chart,
-                  },
-                }}
-              />
-            )}
-        </BodyWrapper>
-      </StyledExpandedChart>
-    </>
-  );
-};
-
-export default withRouter(ExpandedJobChart);
-
-const ExpandedJobHeader: React.FC<{
-  chart: ChartType;
-  jobs: any[];
-  closeChart: () => void;
-  refreshChart: () => Promise<void>;
-  upgradeChart: () => Promise<void>;
-  loadChartWithSpecificRevision: (revision: number) => void;
-  setDisableForm: (disable: boolean) => void;
-  disableRevisions?: boolean;
-}> = ({
-  chart,
-  closeChart,
-  jobs,
-  refreshChart,
-  upgradeChart,
-  loadChartWithSpecificRevision,
-  setDisableForm,
-  disableRevisions,
-}) => (
-    <>
-      <BreadcrumbRow>
-        <Breadcrumb onClick={closeChart}>
-          <ArrowIcon src={leftArrow} />
-          <Wrap>Back</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <HeaderWrapper>
-        <TitleSection icon={chart?.chart.metadata.icon} iconWidth="33px">
-          {chart?.name}
-          <DeploymentType currentChart={chart} />
-          <TagWrapper>
-            Namespace <NamespaceTag>{chart.namespace.startsWith("porter-stack-") ? chart.namespace.replace("porter-stack-", "") : chart.namespace}</NamespaceTag>
-          </TagWrapper>
-        </TitleSection>
-        {chart?.config?.description ? (
-          <Description>{chart?.config?.description}</Description>
-        ) : null}
-
-        <InfoWrapper>
-          {chart?.canonical_name !== "" ? (
-            <Url>
-              <Bolded>Helm Release Name:</Bolded>
-              {chart?.name}
-            </Url>
-          ) : null}
-          <LastDeployed>
-            Run {jobs?.length} times <Dot>•</Dot>Last template update at
-            {" " + readableDate(chart.info.last_deployed)}
-          </LastDeployed>
-        </InfoWrapper>
-        {!disableRevisions ? (
-          <RevisionSection
-            chart={chart}
-            refreshChart={() => 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}
-      </HeaderWrapper>
-    </>
-  );
-
-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;
-`;

+ 0 - 70
dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx

@@ -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<PropsType, StateType> {
-  state = {
-    isExpanded: false,
-  };
-
-  renderContents = () => {
-    if (this.props.components && this.props.components.length > 0) {
-      return (
-        <GraphDisplay
-          setSidebar={this.props.setSidebar}
-          components={this.props.components}
-          isExpanded={this.state.isExpanded}
-          currentChart={this.props.currentChart}
-          showRevisions={this.props.showRevisions}
-        />
-      );
-    }
-
-    return <Loading offset="-30px" />;
-  };
-
-  render() {
-    return <StyledGraphSection>{this.renderContents()}</StyledGraphSection>;
-  }
-}
-
-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);
-    }
-  }
-`;

+ 0 - 163
dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx

@@ -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<PropsType, StateType> {
-  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 (
-        <ResourceTab
-          key={i}
-          handleClick={() =>
-            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 <TabWrapper>{this.renderResourceList()}</TabWrapper>;
-    }
-
-    return <Loading offset="-30px" />;
-  };
-
-  render() {
-    return (
-      <StyledListSection>
-        {this.renderTabs()}
-        <FlexWrapper ref={(element) => (this.wrapperRef = element)}>
-          <YamlWrapper>
-            <YamlEditor
-              value={this.state.yaml}
-              onChange={(e: any) => this.setState({ yaml: e })}
-              height={this.state.wrapperHeight - 2 + "px"}
-              border={true}
-              readOnly={true}
-            />
-          </YamlWrapper>
-        </FlexWrapper>
-      </StyledListSection>
-    );
-  }
-}
-
-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);
-    }
-  }
-`;

+ 0 - 194
dashboard/src/main/home/cluster-dashboard/expanded-chart/NotificationSettingsSection.tsx

@@ -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> = (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(
-        "<token>",
-        {},
-        {
-          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(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-        }
-      )
-      .then(({ data }) => {
-        setHasNotifications(data.length > 0);
-      });
-  }, []);
-
-  const saveChanges = () => {
-    setSaveLoading(true);
-    let payload = {
-      enabled: notificationsOn,
-      ...categories,
-    };
-
-    api
-      .legacyUpdateNotificationConfig(
-        "<token>",
-        {
-          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 (
-    <>
-      <Heading>Notification Settings</Heading>
-      <Helper>Configure notification settings for this application.</Helper>
-      {initLoading ? (
-        <Loading />
-      ) : !hasRelease ? (
-        <Banner type="error">
-          Notifications unavailable. Porter could not find this application in
-          the database.
-        </Banner>
-      ) : (
-        <>
-          {hasNotifications != null && !hasNotifications ? (
-            <Banner type="warning">
-              No integration has been set up for notifications.{" "}
-              <A
-                href={`${window.location.protocol}//${window.location.host}/integrations/slack`}
-              >
-                Connect to Slack
-              </A>
-            </Banner>
-          ) : (
-            <>
-              <CheckboxRow
-                label={"Enable notifications"}
-                checked={notificationsOn}
-                toggle={() => setNotificationsOn(!notificationsOn)}
-                disabled={props.disabled}
-              />
-              {notificationsOn && (
-                <>
-                  <Helper>Send notifications on:</Helper>
-                  {Object.entries(categories).map(
-                    ([k, v]: [string, boolean]) => {
-                      return (
-                        <React.Fragment key={k}>
-                          <CheckboxRow
-                            label={`Deploy ${k}`}
-                            checked={v}
-                            toggle={() =>
-                              setCategories((prev) => {
-                                return {
-                                  ...prev,
-                                  [k]: !v,
-                                };
-                              })
-                            }
-                            disabled={props.disabled}
-                          />
-                        </React.Fragment>
-                      );
-                    }
-                  )}
-                </>
-              )}
-              <br />
-              <SaveButton
-                onClick={() => saveChanges()}
-                text="Save Notification Settings"
-                clearPosition={true}
-                statusPosition={"right"}
-                disabled={props.disabled || initLoading || saveLoading}
-                status={
-                  saveLoading ? "loading" : numSaves > 0 ? "successful" : null
-                }
-                saveText={"Saving . . ."}
-              />
-              <Br />
-            </>
-          )}
-        </>
-      )}
-    </>
-  );
-};
-
-export default NotificationSettingsSection;
-
-const A = styled.a`
-  text-decoration: underline;
-  cursor: pointer;
-  margin-left: 5px;
-`;
-
-const Br = styled.div`
-  width: 100%;
-  height: 10px;
-`;

+ 0 - 558
dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx

@@ -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<PropsType, StateType> {
-  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(
-        "<token>",
-        {},
-        {
-          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(
-        "<token>",
-        {
-          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 (
-        <Tr
-          key={i}
-          onClick={() => this.handleClickRevision(revision)}
-          selected={this.props.chart.version === revision.version}
-        >
-          <Td>{revision.version}</Td>
-          <Td>{readableDate(revision.info.last_deployed)}</Td>
-          <Td>
-            {!imageTag ? (
-              "N/A"
-            ) : isGithubApp && /^[0-9A-Fa-f]{7}$/g.test(imageTag) ? (
-              <A
-                href={`https://github.com/${this.props.chart.git_action_config?.git_repo}/commit/${imageTag}`}
-                target="_blank"
-                onClick={(e) => {
-                  e.stopPropagation();
-                }}
-              >
-                {parsedImageTag}
-              </A>
-            ) : (
-              parsedImageTag
-            )}
-          </Td>
-          <Td>v{revision.chart.metadata.version}</Td>
-          <Td>
-            <RollbackButton
-              disabled={
-                isCurrent ||
-                !this.props.isAuthorized("application", "", [
-                  "get",
-                  "update",
-                ]) ||
-                isStack
-              }
-              onClick={() =>
-                this.setState({ rollbackRevision: revision.version })
-              }
-            >
-              {isCurrent ? "Current" : "Revert"}
-            </RollbackButton>
-          </Td>
-        </Tr>
-      );
-    });
-  };
-
-  renderExpanded = () => {
-    if (this.state.expandRevisions) {
-      return (
-        <TableWrapper>
-          <RevisionsTable>
-            <tbody>
-              <Tr disableHover={true}>
-                <Th>Revision no.</Th>
-                <Th>Timestamp</Th>
-                <Th>
-                  {this.props.chart.git_action_config ? "Commit" : "Image Tag"}
-                </Th>
-                <Th>Template version</Th>
-                <Th>Rollback</Th>
-              </Tr>
-              {this.renderRevisionList()}
-            </tbody>
-          </RevisionsTable>
-        </TableWrapper>
-      );
-    }
-  };
-
-  renderContents = () => {
-    if (this.state.loading) {
-      return (
-        <LoadingPlaceholder>
-          <StatusWrapper>
-            <LoadingGif src={loading} revision={false} /> Updating . . .
-          </StatusWrapper>
-        </LoadingPlaceholder>
-      );
-    }
-
-    let isCurrent =
-      this.props.chart.version === this.state.maxVersion ||
-      this.state.maxVersion === 0;
-    return (
-      <div>
-        {this.state.upgradeVersion && (
-          <Modal
-            onRequestClose={() => this.setState({ upgradeVersion: "" })}
-            width="500px"
-            height="450px"
-          >
-            <UpgradeChartModal
-              currentChart={this.props.chart}
-              closeModal={() => {
-                this.setState({ upgradeVersion: "" });
-              }}
-              onSubmit={() => {
-                this.props.upgradeVersion(this.state.upgradeVersion, () => {
-                  this.setState({ loading: false });
-                });
-                this.setState({ upgradeVersion: "", loading: true });
-              }}
-            />
-          </Modal>
-        )}
-        <RevisionHeader
-          showRevisions={this.props.showRevisions}
-          isCurrent={isCurrent}
-          onClick={() => {
-            if (typeof this.props.toggleShowRevisions === "function") {
-              this.props.toggleShowRevisions();
-            }
-            this.setState((prev) => ({
-              ...prev,
-              expandRevisions: !prev.expandRevisions,
-            }));
-          }}
-        >
-          <RevisionPreview>
-            <i className="material-icons">arrow_drop_down</i>
-            {isCurrent
-              ? `Current version`
-              : `Previewing revision (not deployed)`}{" "}
-            - <Revision>No. {this.props.chart.version}</Revision>
-          </RevisionPreview>
-          {this.props.shouldUpdate && isCurrent && (
-            <div>
-              <RevisionUpdateMessage
-                onClick={(e) => {
-                  e.stopPropagation();
-                  this.setState({ upgradeVersion: this.props.latestVersion });
-                }}
-              >
-                <i className="material-icons">notification_important</i>
-                Template Update Available
-              </RevisionUpdateMessage>
-            </div>
-          )}
-        </RevisionHeader>
-        <RevisionList>{this.renderExpanded()}</RevisionList>
-      </div>
-    );
-  };
-
-  render() {
-    return (
-      <StyledRevisionSection showRevisions={this.state.expandRevisions}>
-        {this.renderContents()}
-        {createPortal(
-          <ConfirmOverlay
-            show={this.state.rollbackRevision && true}
-            message={`Are you sure you want to revert to version ${this.state.rollbackRevision}?`}
-            onYes={this.handleRollback}
-            onNo={() => this.setState({ rollbackRevision: null })}
-          />,
-          document.body
-        )}
-      </StyledRevisionSection>
-    );
-  }
-}
-
-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;
-`;

+ 0 - 491
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -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<void>;
-  setShowDeleteOverlay: (x: boolean) => void;
-  saveButtonText?: string | null;
-};
-
-const SettingsSection: React.FC<PropsType> = ({
-  currentChart,
-  refreshChart,
-  setShowDeleteOverlay,
-  saveButtonText,
-}) => {
-  const [selectedImageUrl, setSelectedImageUrl] = useState<string | null>("");
-  const [selectedTag, setSelectedTag] = useState<string | null>("");
-  const [saveValuesStatus, setSaveValuesStatus] = useState<string | null>(null);
-  const [highlightCopyButton, setHighlightCopyButton] = useState<boolean>(
-    false
-  );
-  const [webhookToken, setWebhookToken] = useState<string>("");
-  const [
-    createWebhookButtonStatus,
-    setCreateWebhookButtonStatus,
-  ] = useState<string>("");
-  const [loadingWebhookToken, setLoadingWebhookToken] = useState<boolean>(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(
-        "<token>",
-        {},
-        {
-          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(
-        "<token>",
-        {
-          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(
-        "<token>",
-        {},
-        {
-          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 <DarkMatter />;
-    }
-
-    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) ? (
-          <>
-            <Heading isAtTop>Source settings</Heading>
-            <Helper>Specify an image tag to use.</Helper>
-            <ImageSelector
-              selectedTag={selectedTag}
-              selectedImageUrl={selectedImageUrl}
-              setSelectedImageUrl={(x: string) => setSelectedImageUrl(x)}
-              setSelectedTag={(x: string) => setSelectedTag(x)}
-              forceExpanded={true}
-              disableImageSelect={false}
-            />
-            {!loadingWebhookToken && (
-              <>
-                <Br />
-                <Br />
-                <Br />
-                <SaveButton
-                  clearPosition={true}
-                  statusPosition="right"
-                  text="Save source settings"
-                  status={saveValuesStatus}
-                  onClick={handleSubmit}
-                />
-              </>
-            )}
-            <Br />
-          </>
-        ) : null}
-
-        <>
-          <Heading>Canonical Name</Heading>
-          <Helper>
-            Set a canonical name for this application (lowercase letters,
-            numbers, and "-" only)
-          </Helper>
-          <CanonicalName release={currentChart} onSave={() => refreshChart()} />
-
-          <Heading>Redeploy Webhook</Heading>
-          <Helper>
-            Programmatically deploy by calling this secret webhook.
-          </Helper>
-
-          {!loadingWebhookToken && !webhookToken.length && (
-            <SaveButton
-              text={"Create Webhook"}
-              status={buttonStatus}
-              onClick={handleCreateWebhookToken}
-              clearPosition={true}
-              statusPosition={"right"}
-              disabled={!isAuthorizedToCreateWebhook}
-            />
-          )}
-          {webhookToken.length > 0 && (
-            <Webhook copiedToClipboard={highlightCopyButton}>
-              <div>{curlWebhook}</div>
-              <CopyToClipboard
-                as="i"
-                text={curlWebhook}
-                onSuccess={() => setHighlightCopyButton(true)}
-                wrapperProps={{
-                  className: "material-icons",
-                  onMouseLeave: () => setHighlightCopyButton(false),
-                }}
-              >
-                content_copy
-              </CopyToClipboard>
-            </Webhook>
-          )}
-        </>
-        <Heading>Application Tags</Heading>
-        <Helper>Add tags for filtering applications.</Helper>
-        <TagSelector release={currentChart} onSave={(val) => 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 (
-    <Wrapper>
-      {!loadingWebhookToken ? (
-        <StyledSettingsSection>
-          {renderWebhookSection()}
-          <NotificationSettingsSection currentChart={currentChart} />
-          {/* Prevent the clone button to be rendered in github deployed charts */}
-          {canBeCloned() && (
-            <>
-              <Heading>Clone deployment</Heading>
-              <Helper>
-                Click the button below to be redirected to the deploy form with
-                all the data prefilled
-              </Helper>
-              <CloneButton as={Link} to={getCloneUrl()}>
-                Clone
-              </CloneButton>
-            </>
-          )}
-
-          <Heading>Additional Settings</Heading>
-          <Button
-            color="#b91133"
-            onClick={() => setShowDeleteOverlay(true)}
-            disabled={!canBeDeleted()}
-          >
-            Delete {currentChart.name}
-          </Button>
-        </StyledSettingsSection>
-      ) : (
-        <Loading />
-      )}
-    </Wrapper>
-  );
-};
-
-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;
-`;

+ 0 - 349
dashboard/src/main/home/cluster-dashboard/expanded-chart/TagSelector.tsx

@@ -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<void>);
-  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(
-        "<token>",
-        { 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<any[]>(
-        "<token>",
-        {},
-        { 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 ? (
-        <CreateTagModal
-          onSave={async (newTag) => {
-            const newValues = [...values, newTag];
-            await onSave(newValues);
-            setValues(newValues);
-          }}
-          onClose={() => setOpenModal(false)}
-          release={release}
-        />
-      ) : null}
-      <Flex>
-        {values.map((val, index) => {
-          return (
-            <Tag color={val.color} key={index}>
-              <Tooltip title={val.name}>
-                <TagText>{val.name}</TagText>
-              </Tooltip>
-              <i className="material-icons" onClick={() => onDelete(index)}>
-                cancel
-              </i>
-            </Tag>
-          );
-        })}
-      </Flex>
-      <SearchSelector
-        options={availableTags}
-        dropdownLabel="Select a tag"
-        renderAddButton={() => (
-          <AddTagButton
-            onClick={(e) => {
-              setOpenModal((prev) => !prev);
-            }}
-          >
-            + Create a new tag
-          </AddTagButton>
-        )}
-        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) => <TagColorBox color={option.color} />}
-      />
-      <Flex
-        style={{
-          marginTop: "25px",
-        }}
-      >
-        <SaveButton
-          helper={hasUnsavedChanges ? "Unsaved changes" : ""}
-          clearPosition
-          statusPosition="right"
-          text="Save changes"
-          onClick={() => handleSave()}
-          status={buttonStatus}
-          disabled={!hasUnsavedChanges || buttonStatus === "loading"}
-        ></SaveButton>
-      </Flex>
-      <Br />
-    </>
-  );
-};
-
-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<void>);
-  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(
-        "<token>",
-        { name, color },
-        {
-          project_id: currentProject.id,
-        }
-      );
-    } catch (error) {
-      setCurrentError(error);
-      setButtonStatus("Couldn't create the tag");
-      return;
-    }
-
-    try {
-      await api.updateReleaseTags(
-        "<token>",
-        { 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 (
-    <Modal title="Create a new tag" onRequestClose={onClose} height="auto">
-      <Helper>
-        Create a new tag and link the release you're currently at to the brand
-        new tag.
-      </Helper>
-
-      <InputRow
-        type="text"
-        label="Tag name"
-        value={name}
-        setValue={(val) => setName(val as string)}
-        isRequired
-        width="300px"
-      ></InputRow>
-      <Label>Tag color</Label>
-      <TwitterPicker
-        triangle="hide"
-        color={color}
-        onChange={(newColor) => setColor(newColor.hex)}
-      ></TwitterPicker>
-
-      <Label style={{ marginTop: "15px" }}>Result</Label>
-      <Tag color={color} style={{ maxWidth: "none", marginTop: "0px" }}>
-        <TagText>{name}</TagText>
-      </Tag>
-      <Flex
-        style={{
-          justifyContent: "flex-end",
-        }}
-      >
-        <SaveButton
-          clearPosition
-          onClick={() => createTag()}
-          text={"Create Tag"}
-          disabled={!name.length || buttonStatus === "loading"}
-        ></SaveButton>
-      </Flex>
-    </Modal>
-  );
-};
-
-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};
-`;

+ 0 - 151
dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx

@@ -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<PropsType, StateType> {
-  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(
-        "<token>",
-        {
-          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 (
-      <StyledValuesYaml>
-        <Wrapper>
-          <YamlEditor
-            value={this.state.values}
-            onChange={(e: any) => this.setState({ values: e })}
-            readOnly={this.props.disabled}
-            height="calc(100vh - 412px)"
-          />
-        </Wrapper>
-        {!this.props.disabled && (
-          <SaveButton
-            text="Update values"
-            onClick={this.handleSaveValues}
-            status={this.state.saveValuesStatus}
-            statusPosition="right"
-            clearPosition={true}
-            makeFlush={true}
-          />
-        )}
-      </StyledValuesYaml>
-    );
-  }
-}
-
-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);
-    }
-  }
-`;

+ 0 - 598
dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx

@@ -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<Props> = ({
-  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(
-        "<token>",
-        {
-          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<UpdateBuildconfigResponse>(
-        "<token>",
-        { ...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(
-        "<token>",
-        {
-          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 (
-    <Wrapper>
-      {isPreviousVersion ? (
-        <DisabledOverlay>
-          Build config is disabled when reviewing past versions. Please go to
-          the current revision to update your app build configuration.
-        </DisabledOverlay>
-      ) : null}
-      <StyledSettingsSection blurContent={isPreviousVersion}>
-        {/* {reRunError !== null ? (
-        <AlertCard>
-          <AlertCardIcon className="material-icons">error</AlertCardIcon>
-          <AlertCardContent className="content">
-            <AlertCardTitle className="title">
-              {reRunError.title}
-            </AlertCardTitle>
-            {reRunError.description}
-            {runningWorkflowURL.length ? (
-              <>
-                {" "}
-                To go to the workflow{" "}
-                <DynamicLink to={runningWorkflowURL} target="_blank">
-                  click here
-                </DynamicLink>
-              </>
-            ) : null}
-          </AlertCardContent>
-          <AlertCardAction
-            onClick={() => {
-              setReRunError(null);
-              setRunningWorkflowURL("");
-            }}
-          >
-            <span className="material-icons">close</span>
-          </AlertCardAction>
-        </AlertCard>
-      ) : null} */}
-        <Heading isAtTop>
-          <ExpandHeader
-            onClick={() => setGitHubSettingsExpanded(!gitHubSettingsExpanded)}
-            isExpanded={!gitHubSettingsExpanded}
-          >
-            Github Settings
-            <i className="material-icons">arrow_drop_down</i>
-          </ExpandHeader>
-        </Heading>
-        {gitHubSettingsExpanded && (
-          <div>
-            <InputRow
-              disabled={true}
-              label="Git repository"
-              type="text"
-              width="100%"
-              value={chart.git_action_config?.git_repo}
-            />
-            <InputRow
-              disabled={true}
-              label="Branch"
-              type="text"
-              width="100%"
-              value={currentBranch}
-            />
-            {chart.git_action_config.dockerfile_path && (
-              <InputRow
-                disabled={true}
-                label="Dockerfile path (absolute path)"
-                type="text"
-                width="100%"
-                value={chart.git_action_config.dockerfile_path}
-              />
-            )}
-            {!chart.git_action_config.dockerfile_path && (
-              <InputRow
-                disabled={true}
-                label={
-                  chart.git_action_config.dockerfilePath
-                    ? "Docker build context"
-                    : "Application folder"
-                }
-                type="text"
-                width="100%"
-                value={chart.git_action_config.folder_path}
-              />
-            )}
-          </div>
-        )}
-        <Heading>
-          <ExpandHeader
-            onClick={() => setBranchSelectionExpanded(!branchSelectionExpanded)}
-            isExpanded={!branchSelectionExpanded}
-          >
-            Select default branch
-            <i className="material-icons">arrow_drop_down</i>
-          </ExpandHeader>
-        </Heading>
-        {branchSelectionExpanded && (
-          <div>
-            {/* Select default branch content */}
-            <Helper>
-              Change the default branch the deployments will be made from.
-            </Helper>
-            <Banner>
-              You must also update the deploy branch in your GitHub Action file.
-            </Banner>
-            <BranchList
-              actionConfig={currentActionConfig}
-              setBranch={setCurrentBranch}
-              currentBranch={currentBranch}
-            />
-          </div>
-        )}
-
-        <Heading>
-          <ExpandHeader
-            onClick={() => setEnvVariablesExpanded(!envVariablesExpanded)}
-            isExpanded={!envVariablesExpanded}
-          >
-            Build environment variables
-            <i className="material-icons">arrow_drop_down</i>
-          </ExpandHeader>
-        </Heading>
-
-        {envVariablesExpanded && (
-          <div>
-            <KeyValueArray
-              values={envVariables}
-              envLoader
-              externalValues={{
-                namespace: chart.namespace,
-                clusterId: currentCluster.id,
-              }}
-              setValues={(values) => {
-                setEnvVariables(values);
-              }}
-            ></KeyValueArray>
-          </div>
-        )}
-
-        {!chart.git_action_config.dockerfile_path ? (
-          <>
-            <Heading>
-              <ExpandHeader
-                onClick={() =>
-                  setBuildpackSettingsExpanded(!buildpackSettingsExpanded)
-                }
-                isExpanded={!buildpackSettingsExpanded}
-              >
-                Buildpacks settings
-                <i className="material-icons">arrow_drop_down</i>
-              </ExpandHeader>
-            </Heading>
-            {buildpackSettingsExpanded &&
-              !chart.git_action_config.dockerfile_path && (
-                <div>
-                  <BuildpackConfigSection
-                    ref={buildpackConfigRef}
-                    currentChart={chart}
-                    actionConfig={currentActionConfig}
-                  />
-                </div>
-              )}
-          </>
-        ) : null}
-        <SaveButtonWrapper>
-          <MultiSaveButton
-            options={[
-              {
-                text: "Save",
-                onClick: handleSave,
-                description:
-                  "Save the build settings to be used in the next workflow run",
-              },
-              {
-                text: "Save and Redeploy",
-                onClick: handleSaveAndReDeploy,
-                description:
-                  "Immediately trigger a workflow run with updated build settings",
-              },
-            ]}
-            disabled={false}
-            makeFlush={true}
-            clearPosition={true}
-            statusPosition="left"
-            expandTo="left"
-            saveText=""
-            status={buttonStatus}
-          ></MultiSaveButton>
-        </SaveButtonWrapper>
-      </StyledSettingsSection>
-    </Wrapper>
-  );
-};
-
-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;
-`;

+ 0 - 554
dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/_BuildpackConfigSection.tsx

@@ -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<DetectedBuildpack[]>(null);
-  const [selectedBuilder, setSelectedBuilder] = useState<string>(null);
-
-  const [stacks, setStacks] = useState<string[]>(null);
-  const [selectedStack, setSelectedStack] = useState<string>(null);
-
-  const [selectedBuildpacks, setSelectedBuildpacks] = useState<Buildpack[]>([]);
-  const [availableBuildpacks, setAvailableBuildpacks] = useState<Buildpack[]>(
-    []
-  );
-
-  const [isLoading, setIsLoading] = useState(true);
-  const [error, setError] = useState(false);
-
-  const state = useRef<null | {
-    [builder: string]: {
-      stack: string;
-      selectedBuildpacks: Buildpack[];
-      availableBuildpacks: Buildpack[];
-    };
-  }>(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<DetectBuildpackResponse>(
-        "<token>",
-        {
-          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<DetectBuildpackResponse>(
-      "<token>",
-      {
-        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 (
-        <StyledCard>Buildpacks will be automatically detected.</StyledCard>
-      );
-    }
-
-    if (!buildpacks.length && action === "add") {
-      return (
-        <StyledCard>
-          No additional buildpacks are available. You can add a custom buildpack
-          below.
-        </StyledCard>
-      );
-    }
-
-    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 (
-        <StyledCard key={i}>
-          <ContentContainer>
-            <Icon disableMarginRight={disableIcon} className={icon} />
-            <EventInformation>
-              <EventName>{buildpack?.name}</EventName>
-            </EventInformation>
-          </ContentContainer>
-          <ActionContainer>
-            {action === "add" && (
-              <DeleteButton
-                onClick={() => handleAddBuildpack(buildpack.buildpack)}
-              >
-                <span className="material-icons-outlined">add</span>
-              </DeleteButton>
-            )}
-            {action === "remove" && (
-              <DeleteButton
-                onClick={() => handleRemoveBuildpack(buildpack.buildpack)}
-              >
-                <span className="material-icons">delete</span>
-              </DeleteButton>
-            )}
-          </ActionContainer>
-        </StyledCard>
-      );
-    });
-  };
-
-  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 (
-      <div style={{ marginTop: "20px" }}>
-        <Loading />
-      </div>
-    );
-  }
-
-  if (error) {
-    return (
-      <div style={{ marginTop: "20px" }}>
-        <Placeholder>
-          <div>
-            <h2>Couldn't retrieve buildpacks.</h2>
-            <p>
-              Check if the branch exists and the Porter App has enough
-              permissions on the repository.
-            </p>
-          </div>
-        </Placeholder>
-      </div>
-    );
-  }
-
-  return (
-    <BuildpackConfigurationContainer>
-      <>
-        <SelectRow
-          value={selectedBuilder}
-          width="100%"
-          options={builderOptions}
-          setActiveValue={(option) => handleSelectBuilder(option)}
-          label="Select a builder"
-        />
-        <SelectRow
-          value={selectedStack}
-          width="100%"
-          options={stackOptions}
-          setActiveValue={(option) => setSelectedStack(option)}
-          label="Select your stack"
-        />
-        <Helper>
-          The following buildpacks were automatically detected. You can also
-          manually add/remove buildpacks.
-        </Helper>
-        <>{renderBuildpacksList(selectedBuildpacks, "remove")}</>
-        <Helper>Available buildpacks:</Helper>
-        <>{renderBuildpacksList(availableBuildpacks, "add")}</>
-        <Helper>
-          You may also add buildpacks by directly providing their GitHub links
-          or links to ZIP files that contain the buildpack source code.
-        </Helper>
-        <AddCustomBuildpackForm onAdd={handleAddCustomBuildpack} />
-      </>
-    </BuildpackConfigurationContainer>
-  );
-});
-
-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;
-  }
-`;

+ 0 - 29
dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/types.ts

@@ -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;
-};

+ 0 - 391
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx

@@ -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<Props> = ({
-  controller,
-  selectPod,
-  isFirst,
-  isLast,
-  selectors,
-  setPodError,
-  selectedPod,
-  onUpdate,
-}) => {
-  const [pods, setPods] = useState<ControllerTabPodType[]>([]);
-  const [rawPodList, setRawPodList] = useState<any[]>([]);
-  const [podPendingDelete, setPodPendingDelete] = useState<any>(null);
-  const [available, setAvailable] = useState<number>(null);
-  const [total, setTotal] = useState<number>(null);
-  const [userSelectedPod, setUserSelectedPod] = useState<boolean>(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(
-        "<token>",
-        {
-          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<ControllerTabPodType>((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(
-        "<token>",
-        {},
-        {
-          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<Array<Array<ControllerTabPodType>>>(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 (
-        <PodRow
-          key={i}
-          pod={pod}
-          isSelected={currentSelectedPod?.name === pod?.name}
-          podStatus={status}
-          isLastItem={i === arr.length - 1}
-          onTabClick={() => {
-            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 (
-    <ResourceTab
-      label={controller.kind}
-      // handle CronJob case
-      name={controller.metadata?.name || controller.name}
-      status={{ label: currentControllerStatus, available, total }}
-      isLast={isLast}
-      expanded={isFirst}
-    >
-      {!!replicaSetArray.length &&
-        replicaSetArray.map((subArray, index) => {
-          const firstItem = subArray[0];
-          return (
-            <div key={firstItem.replicaSetName + index}>
-              <ReplicaSetContainer>
-                <ReplicaSetName>
-                  {firstItem?.revisionNumber &&
-                    firstItem?.revisionNumber.toString() != "N/A" && (
-                      <Bold>Revision {firstItem.revisionNumber}:</Bold>
-                    )}{" "}
-                  {firstItem.replicaSetName}
-                </ReplicaSetName>
-              </ReplicaSetContainer>
-              {mapPods(subArray)}
-            </div>
-          );
-        })}
-      {!replicaSetArray.length && mapPods(pods)}
-      <ConfirmOverlay
-        message="Are you sure you want to delete this pod?"
-        show={podPendingDelete}
-        onYes={() => handleDeletePod(podPendingDelete)}
-        onNo={() => setPodPendingDelete(null)}
-      />
-    </ResourceTab>
-  );
-};
-
-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;
-`;

+ 0 - 219
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx

@@ -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> = (props) => {
-  const [status, setStatus] = useState<DeployStatus>("Deployed");
-  const [percentage, setPercentage] = useState("0%");
-  const [isExpanded, setIsExpanded] = useState(false);
-
-  const wrapperRef = useRef<HTMLInputElement>(null);
-  const parentRef = useRef<HTMLInputElement>(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 (
-    <>
-      <StyledDeployStatusSection
-        onClick={() => setIsExpanded(!isExpanded)}
-        ref={parentRef}
-        isExpanded={isExpanded}
-      >
-        {status === "Deploying" ? (
-          <>
-            <StatusCircle percentage={percentage} />
-            {status}
-          </>
-        ) : (
-          <StatusWrapper>
-            <StatusColor status={status} />
-            {status}
-          </StatusWrapper>
-        )}
-      </StyledDeployStatusSection>
-      <DropdownWrapper expanded={isExpanded}>
-        <Dropdown ref={wrapperRef}>
-          <PodDropdown
-            currentChart={props.chart}
-            onUpdate={onUpdate}
-            // Allow users to navigate to pod logs upon clicking the pod
-            onSelectPod={(pod: any) => {
-              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"],
-              });
-            }}
-          />
-        </Dropdown>
-      </DropdownWrapper>
-    </>
-  );
-};
-
-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;
-`;

+ 0 - 149
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx

@@ -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<Props> = ({
-  currentChart,
-  selectors,
-  onUpdate,
-  onSelectPod,
-}) => {
-  const [selectedPod, setSelectedPod] = useState<any>({});
-  const [controllers, setControllers] = useState<any[]>([]);
-  const [isLoading, setIsLoading] = useState<boolean>(true);
-  const [podError, setPodError] = useState<string>("");
-
-  const { currentProject, currentCluster, setCurrentError } = useContext(
-    Context
-  );
-
-  useEffect(() => {
-    let isSubscribed = true;
-    api
-      .getChartControllers(
-        "<token>",
-        {},
-        {
-          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 (
-        <ControllerTab
-          // handle CronJob case
-          key={c.metadata?.uid || c.uid}
-          selectedPod={selectedPod}
-          selectPod={(pod: any, userSelected) => {
-            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 (
-        <NoControllers>
-          <Loading />
-        </NoControllers>
-      );
-    }
-    if (controllers?.length > 0) {
-      return <TabWrapper>{renderTabs()}</TabWrapper>;
-    }
-
-    return (
-      <NoControllers>
-        <i className="material-icons">category</i>
-        No objects to display. This might happen while your app is still
-        deploying.
-      </NoControllers>
-    );
-  };
-
-  return <StyledStatusSection>{renderStatusSection()}</StyledStatusSection>;
-};
-
-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;
-  }
-`;

+ 0 - 223
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodRow.tsx

@@ -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<PodRowProps> = ({
-  pod,
-  isSelected,
-  onTabClick,
-  onDeleteClick,
-  isLastItem,
-  podStatus,
-}) => {
-  const [showTooltip, setShowTooltip] = useState(false);
-
-  return (
-    <Tab key={pod?.name} onClick={onTabClick}>
-      <Gutter>
-        <Rail />
-        <Circle />
-        <Rail lastTab={isLastItem} />
-      </Gutter>
-      <Name
-      // onMouseOver={() => {
-      //   setShowTooltip(true);
-      // }}
-      // onMouseOut={() => {
-      //   setShowTooltip(false);
-      // }}
-      >
-        {pod?.name}
-      </Name>
-      {showTooltip && (
-        <Tooltip>
-          {pod?.name}
-          <Grey>Restart count: {pod.restartCount}</Grey>
-          <Grey>Created on: {pod.podAge}</Grey>
-          {podStatus === "failed" ? (
-            <FailedStatusContainer>
-              <Grey>
-                Failure Reason: {pod?.containerStatus?.state?.waiting?.reason}
-              </Grey>
-              <Grey>{pod?.containerStatus?.state?.waiting?.message}</Grey>
-            </FailedStatusContainer>
-          ) : null}
-        </Tooltip>
-      )}
-
-      <Status>
-        {podStatus}
-        <StatusColor status={podStatus} />
-        {/* {podStatus === "failed" && (
-          <CloseIcon
-            className="material-icons-outlined"
-            onClick={onDeleteClick}
-          >
-            close
-          </CloseIcon>
-        )} */}
-      </Status>
-    </Tab>
-  );
-};
-
-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;
-`;

+ 0 - 316
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ResourceTab.tsx

@@ -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<PropsType, StateType> {
-  state = {
-    expanded: this.props.expanded || false,
-    showTooltip: false,
-  };
-
-  renderDropdownIcon = () => {
-    if (this.props.children) {
-      return (
-        <DropdownIcon expanded={this.state.expanded}>
-          <i className="material-icons">arrow_right</i>
-        </DropdownIcon>
-      );
-    }
-  };
-
-  renderIcon = (kind: string) => {
-    let icon = "tonality";
-    if (Object.keys(kindToIcon).includes(kind)) {
-      icon = kindToIcon[kind];
-    }
-
-    return (
-      <IconWrapper>
-        <i className="material-icons">{icon}</i>
-      </IconWrapper>
-    );
-  };
-
-  renderTooltip = (x: string): JSX.Element | undefined => {
-    if (this.state.showTooltip) {
-      return <Tooltip>{x}</Tooltip>;
-    }
-  };
-
-  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 (
-        <Status>
-          {this.getStatusText()}
-
-          <StatusColor status={status.label} />
-        </Status>
-      );
-    }
-  };
-
-  renderExpanded = () => {
-    if (this.props.children && this.state.expanded) {
-      return <ExpandWrapper>{this.props.children}</ExpandWrapper>;
-    }
-  };
-
-  render() {
-    let {
-      label,
-      name,
-      children,
-      isLast,
-      handleClick,
-      selected,
-      status,
-      roundAllCorners,
-    } = this.props;
-    return (
-      <StyledResourceTab
-        isLast={isLast}
-        onClick={() => handleClick && handleClick()}
-        roundAllCorners={roundAllCorners}
-      >
-        <ResourceHeader
-          hasChildren={children && true}
-          expanded={this.state.expanded || selected}
-          onClick={() => {
-            if (children) {
-              this.setState({ expanded: !this.state.expanded });
-            }
-          }}
-        >
-          <Info>
-            {this.renderDropdownIcon()}
-            <Metadata hasStatus={status && true}>
-              {this.renderIcon(label)}
-              {label}
-              <ResourceName
-                showKindLabels={true}
-                onMouseOver={() => {
-                  this.setState({ showTooltip: true });
-                }}
-                onMouseOut={() => {
-                  this.setState({ showTooltip: false });
-                }}
-              >
-                {name}
-              </ResourceName>
-              {this.renderTooltip(name)}
-            </Metadata>
-          </Info>
-          {this.renderStatus()}
-        </ResourceHeader>
-        {this.renderExpanded()}
-      </StyledResourceTab>
-    );
-  }
-}
-
-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);
-      }
-    }
-  }
-`;

+ 0 - 66
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts

@@ -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];
-
-};

+ 0 - 697
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventList.tsx

@@ -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 (
-      <LogsLoadWrapper>
-        <Loading />
-      </LogsLoadWrapper>
-    );
-  }
-
-  return (
-    <LogsSectionWrapper>
-      <StyledLogsSection>
-        {logs?.map((log, i) => {
-          return (
-            <LogSpan key={[log.lineNumber, i].join(".")}>
-              <span className="line-number">{log.lineNumber}.</span>
-              <span className="line-timestamp">
-                {dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")}
-              </span>
-              <LogOuter key={[log.lineNumber, i].join(".")}>
-                {log.line?.map((ansi, j) => {
-                  if (ansi.clearLine) {
-                    return null;
-                  }
-
-                  return (
-                    <LogInnerSpan
-                      key={[log.lineNumber, i, j].join(".")}
-                      ansi={ansi}
-                    >
-                      {ansi.content.replace(/ /g, "\u00a0")}
-                    </LogInnerSpan>
-                  );
-                })}
-              </LogOuter>
-            </LogSpan>
-          );
-        })}
-      </StyledLogsSection>
-      <ViewLogsWrapper>
-        <DocsLink
-          onClick={(e) => {
-            e.preventDefault();
-            e.stopPropagation();
-            onViewMore();
-          }}
-        >
-          View complete log history
-          <i className="material-icons">open_in_new</i>{" "}
-        </DocsLink>
-      </ViewLogsWrapper>
-    </LogsSectionWrapper>
-  );
-};
-
-const EventList: React.FC<Props> = ({ filters, namespace, setLogData }) => {
-  const { currentProject, currentCluster } = useContext(Context);
-  const [events, setEvents] = useState([]);
-  const [logs, setLogs] = useState<Log[]>([]);
-  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(
-        "<token>",
-        {
-          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("<token>", filters, {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-        })
-        .then((res) => {
-          setEvents(res.data.events);
-          setIsLoading(false);
-          setRefresh(false);
-        });
-    } else {
-      api
-        .listPorterEvents("<token>", 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(
-        "<token>",
-        {
-          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(
-            "<token>",
-            {
-              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 <Loading />;
-    }
-
-    return (
-      <>
-        <Message>
-          <img src={document} />
-          {expandedIncidentEvents[0].detail}
-        </Message>
-        {expandedEvent.should_view_logs ? (
-          <ExpandedIncidentLogs
-            logs={logs}
-            onViewMore={() => redirectToLogs(expandedEvent)}
-          />
-        ) : null}
-      </>
-    );
-  };
-
-  const renderIncidentSummaryCell = (incident: any) => {
-    return (
-      <NameWrapper>
-        <AlertIcon src={danger} />
-        {incident.short_summary}
-        {incident.severity === "normal" ? (
-          <></>
-        ) : (
-          <Status color="#cc3d42">Critical</Status>
-        )}
-      </NameWrapper>
-    );
-  };
-
-  const renderDeploymentFinishedCell = (release: any) => {
-    return (
-      <NameWrapper>
-        <AlertIcon src={rocket} />
-        Revision {release.revision} was successfully deployed
-      </NameWrapper>
-    );
-  };
-
-  const renderJobStartedCell = (timestamp: any) => {
-    return (
-      <NameWrapper>
-        <AlertIcon src={time} />
-        The job started at {readableDate(timestamp)}
-      </NameWrapper>
-    );
-  };
-
-  const renderJobFinishedCell = (timestamp: any) => {
-    return (
-      <NameWrapper>
-        <AlertIcon src={time} />
-        The job finished at {readableDate(timestamp)}
-      </NameWrapper>
-    );
-  };
-
-  const columns = React.useMemo(
-    () => [
-      {
-        Header: "Monitors",
-        columns: [
-          {
-            Header: "Description",
-            accessor: "type",
-            width: 500,
-            Cell: ({ row }: CellProps<any>) => {
-              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<any>) => {
-              return <Flex>{relativeDate(row.original.timestamp)}</Flex>;
-            },
-          },
-          {
-            id: "details",
-            accessor: "",
-            width: 20,
-            Cell: ({ row }: CellProps<any>) => {
-              if (row.original.type == "incident") {
-                return (
-                  <TableButton
-                    onClick={() => {
-                      setExpandedEvent(row.original.data);
-                    }}
-                  >
-                    <Icon src={info} />
-                    Details
-                  </TableButton>
-                );
-              }
-
-              return null;
-            },
-          },
-        ],
-      },
-    ],
-    []
-  );
-
-  return (
-    <>
-      {expandedEvent && (
-        <Modal
-          onRequestClose={() => {
-            setExpandedEvent(null);
-            setLogs([]);
-          }}
-          height="auto"
-        >
-          <TitleSection icon={danger}>
-            <Text>{expandedEvent.release_name}</Text>
-          </TitleSection>
-          <InfoRow>
-            <InfoTab>
-              <img src={time} /> <Bold>Last updated:</Bold>
-              {readableDate(expandedEvent.updated_at)}
-            </InfoTab>
-            <InfoTab>
-              <img src={info} /> <Bold>Status:</Bold>
-              <Capitalize>{expandedEvent.status}</Capitalize>
-            </InfoTab>
-            <InfoTab>
-              <img src={status} /> <Bold>Priority:</Bold>{" "}
-              <Capitalize>{expandedEvent.severity}</Capitalize>
-            </InfoTab>
-          </InfoRow>
-          {expandedEvent?.porter_doc_link && (
-            <DocsLink target="_blank" href={expandedEvent?.porter_doc_link}>
-              View troubleshooting steps
-              <i className="material-icons">open_in_new</i>{" "}
-            </DocsLink>
-          )}
-          {renderExpandedEventMessage()}
-        </Modal>
-      )}
-      {isLoading ? (
-        <LoadWrapper>
-          <Loading />
-        </LoadWrapper>
-      ) : (
-        <TableWrapper>
-          <Table 
-            columns={columns} 
-            data={events} 
-            placeholder="No events found."
-          />
-          <FlexRow>
-            <Flex>
-              <Button
-                onClick={() => {
-                  setIsLoading(true);
-                  setRefresh(true);
-                }}
-              >
-                <i className="material-icons">autorenew</i>
-                Refresh
-              </Button>
-            </Flex>
-          </FlexRow>
-        </TableWrapper>
-      )}
-    </>
-  );
-};
-
-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;
-`;

+ 0 - 265
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx

@@ -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<Props> = ({
-  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("<token>", {}, { 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("<token>", 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("<token>", {}, { 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 (
-      <Placeholder>
-        <Header>Installing agent...</Header>
-      </Placeholder>
-    );
-  }
-
-  if (isLoading) {
-    return (
-      <Placeholder>
-        <Loading />
-      </Placeholder>
-    );
-  }
-
-  if (!hasPorterAgent) {
-    return (
-      <Placeholder>
-        <div>
-          <Header>We couldn't detect the Porter agent on your cluster</Header>
-          In order to use the events tab, you need to install the Porter agent.
-          <InstallPorterAgentButton onClick={() => triggerInstall()}>
-            <i className="material-icons">add</i> Install Porter agent
-          </InstallPorterAgentButton>
-        </div>
-      </Placeholder>
-    );
-  }
-
-  return (
-    <EventsPageWrapper>
-      <EventList
-        namespace={currentChart.namespace}
-        setLogData={setLogData}
-        filters={getFilters()}
-      />
-    </EventsPageWrapper>
-  );
-};
-
-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;
-`;

+ 0 - 155
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/styles.ts

@@ -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;
-  }
-`;

+ 0 - 100
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Edge.tsx

@@ -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<PropsType, StateType> {
-  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 (
-      <StyledEdge
-        length={length}
-        cx={cx}
-        cy={cy}
-        angle={angle}
-        onMouseEnter={() => setCurrentEdge(edge)}
-        onMouseLeave={() => setCurrentEdge(null)}
-        type={edge.type}
-      >
-        {this.state.showArrowHead ? (
-          <ArrowHead color={edgeColors[edge.type]} />
-        ) : null}
-        <VisibleLine color={edgeColors[edge.type]} />
-      </StyledEdge>
-    );
-  }
-}
-
-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;
-    }
-  }
-`;

+ 0 - 750
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx

@@ -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<PropsType, StateType> {
-  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 (
-        <Node
-          key={i}
-          node={node}
-          originX={originX}
-          originY={originY}
-          nodeMouseDown={() => 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 (
-        <Edge
-          key={i}
-          originX={this.state.originX}
-          originY={this.state.originY}
-          x1={this.state.nodes[edge.source].x}
-          y1={this.state.nodes[edge.source].y}
-          x2={this.state.nodes[edge.target].x}
-          y2={this.state.nodes[edge.target].y}
-          edge={edge}
-          setCurrentEdge={(edge: EdgeType) =>
-            this.setState({ currentEdge: edge })
-          }
-        />
-      );
-    });
-  };
-
-  renderSelectRegion = () => {
-    if (this.state.anchorX && this.state.anchorY) {
-      return (
-        <SelectRegion
-          anchorX={this.state.anchorX}
-          anchorY={this.state.anchorY}
-          originX={this.state.originX}
-          originY={this.state.originY}
-          cursorX={this.state.cursorX}
-          cursorY={this.state.cursorY}
-        />
-      );
-    }
-  };
-
-  render() {
-    return (
-      <StyledGraphDisplay
-        isExpanded={this.state.isExpanded}
-        ref={(element) => (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()}
-
-        <ButtonSection
-          onMouseEnter={() => this.setState({ suppressDisplay: true })}
-          onMouseLeave={() => this.setState({ suppressDisplay: false })}
-        >
-          <ToggleLabel
-            onClick={() =>
-              this.setState({ showKindLabels: !this.state.showKindLabels })
-            }
-          >
-            <Checkbox checked={this.state.showKindLabels}>
-              <i className="material-icons">done</i>
-            </Checkbox>
-            Show Type
-          </ToggleLabel>
-          {/*
-          <ExpandButton onClick={this.toggleExpanded}>
-            <i className="material-icons">
-              {this.state.isExpanded ? "close_fullscreen" : "open_in_full"}
-            </i>
-          </ExpandButton>
-          */}
-        </ButtonSection>
-        <InfoPanel
-          setSuppressDisplay={(x: boolean) =>
-            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}
-        />
-        <ZoomPanel btnZoomIn={this.btnZoomIn} btnZoomOut={this.btnZoomOut} />
-      </StyledGraphDisplay>
-    );
-  }
-}
-
-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" : ""};
-`;

+ 0 - 245
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx

@@ -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<PropsType, StateType> {
-  state = {
-    wrapperHeight: 0,
-  };
-
-  renderIcon = (kind: string) => {
-    let icon = "tonality";
-    if (Object.keys(kindToIcon).includes(kind)) {
-      icon = kindToIcon[kind];
-    }
-
-    return (
-      <IconWrapper>
-        <i className="material-icons">{icon}</i>
-      </IconWrapper>
-    );
-  };
-
-  renderColorBlock = (type: string) => {
-    return <ColorBlock color={edgeColors[type]} />;
-  };
-
-  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 (
-        <Wrapped>
-          <Div>
-            {this.renderIcon(openedNode.kind)}
-            {openedNode.kind}
-            <ResourceName>{openedNode.name}</ResourceName>
-          </Div>
-          <YamlWrapper ref={(element) => (this.wrapperRef = element)}>
-            <YamlEditor
-              value={yaml.dump(openedNode.RawYAML)}
-              readOnly={true}
-              height={this.state.wrapperHeight + "px"}
-            />
-          </YamlWrapper>
-        </Wrapped>
-      );
-    } else if (currentNode) {
-      return (
-        <Div>
-          {this.renderIcon(currentNode.kind)}
-          {currentNode.kind}
-          <ResourceName>{currentNode.name}</ResourceName>
-        </Div>
-      );
-    } else if (currentEdge) {
-      return (
-        <EdgeInfo>
-          {this.renderColorBlock(currentEdge.type)}
-          {this.renderEdgeMessage(currentEdge)}
-        </EdgeInfo>
-      );
-    }
-
-    return (
-      <Div>
-        <IconWrapper>
-          <i className="material-icons">info</i>
-        </IconWrapper>
-        Hover over a node or edge to display info.
-      </Div>
-    );
-  };
-
-  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 (
-      <StyledInfoPanel
-        expanded={Boolean(openedNode)}
-        onMouseEnter={openedNode ? () => setSuppressDisplay(true) : null}
-        onMouseLeave={openedNode ? () => setSuppressDisplay(false) : null}
-      >
-        {this.renderContents()}
-
-        {openedNode ? (
-          <i onClick={closeNode} className="material-icons">
-            close
-          </i>
-        ) : null}
-      </StyledInfoPanel>
-    );
-  }
-}
-
-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;
-    }
-  }
-`;

+ 0 - 135
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx

@@ -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<PropsType, StateType> {
-  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 (
-      <StyledNode
-        x={Math.round(originX + x - w / 2)}
-        y={Math.round(originY - y - h / 2)}
-        w={Math.round(w)}
-        h={Math.round(h)}
-      >
-        <Kind>
-          <StyledMark>{this.props.showKindLabels ? kind : null}</StyledMark>
-        </Kind>
-        <NodeBlock
-          onMouseDown={nodeMouseDown}
-          onMouseUp={nodeMouseUp}
-          onMouseEnter={() => this.props.setCurrentNode(this.props.node)}
-          onMouseLeave={() => this.props.setCurrentNode(null)}
-          isActive={isActive}
-          isOpen={this.props.isOpen}
-        >
-          <i className="material-icons">{icon}</i>
-        </NodeBlock>
-        <NodeLabel>
-          <StyledMark>{name}</StyledMark>
-        </NodeLabel>
-      </StyledNode>
-    );
-  }
-}
-
-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;
-`;

+ 0 - 60
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/SelectRegion.tsx

@@ -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<PropsType, StateType> {
-  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 (
-      <StyledSelectRegion
-        x={Math.round(originX + x)}
-        y={Math.round(originY - y)}
-        w={w}
-        h={h}
-      />
-    );
-  }
-}
-
-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;
-`;

+ 0 - 91
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/ZoomPanel.tsx

@@ -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<PropsType, StateType> {
-  state = {
-    wrapperHeight: 0,
-  };
-
-  wrapperRef: any = React.createRef();
-
-  componentDidMount() {
-    this.setState({ wrapperHeight: this.wrapperRef.offsetHeight });
-  }
-
-  renderContents = () => {
-    return (
-      <Div>
-        <IconWrapper onClick={this.props.btnZoomIn}>
-          <i className="material-icons">add</i>
-        </IconWrapper>
-        <ZoomBreaker />
-        <IconWrapper onClick={this.props.btnZoomOut}>
-          <i className="material-icons">remove</i>
-        </IconWrapper>
-      </Div>
-    );
-  };
-
-  render() {
-    return <StyledZoomer>{this.renderContents()}</StyledZoomer>;
-  }
-}
-
-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;
-`;

+ 0 - 9
dashboard/src/main/home/cluster-dashboard/expanded-chart/incidents/DisabledNamespaces.ts

@@ -1,9 +0,0 @@
-export const DisabledNamespacesForIncidents = [
-  "cert-manager",
-  "ingress-nginx",
-  "kube-node-lease",
-  "kube-public",
-  "kube-system",
-  "monitoring",
-  "porter-agent-system",
-];

+ 0 - 61
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ConnectToJobInstructionsModal.tsx

@@ -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 (
-    <Modal
-      onRequestClose={() => 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&nbsp;
-      <a href={"https://docs.porter.run/cli/installation"} target="_blank">
-        here
-      </a>
-      ).
-      <br />
-      <br />
-      Run the following commands to set your current project and cluster
-      <Code>
-        porter config set-project {currentProject.id}
-        <br />
-        porter config set-cluster {currentCluster.id}
-      </Code>
-      <br />
-      Run the following line of code, and make sure to change the command to
-      something your container can run:
-      <Code>porter run {chartName || "[APP-NAME]"} -- [COMMAND]</Code>
-      Note that this will create a copy of the most recent job run for this
-      template.
-    </Modal>
-  );
-};
-
-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;
-`;

+ 0 - 555
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx

@@ -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 <Status color="#cc3d42">Deleting</Status>;
-  }
-
-  if (job.status?.succeeded >= 1) {
-    if (time) {
-      return <Status color="#38a88a">Succeeded at {time}</Status>;
-    }
-
-    return <Status color="#38a88a">Succeeded</Status>;
-  }
-
-  if (job.status?.conditions) {
-    if (job.status?.conditions[0]?.reason == "DeadlineExceeded") {
-      return <Status color="#cc3d42">Timed Out</Status>;
-    }
-  }
-
-  if (job.status?.failed >= 1) {
-    return <Status color="#cc3d42">Failed</Status>;
-  }
-
-  if (job.status?.active >= 1) {
-    // determine the status from the pod
-    return pod ? (
-      <Status color="#ffffff11">{capitalize(getPodStatus(pod?.status))}</Status>
-    ) : (
-      <Status color="#ffffff11">Running</Status>
-    );
-  }
-
-  return <Status color="#ffffff11">Running</Status>;
-};
-
-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<ExpandedJobRunTabs>(
-    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 <Placeholder>No config was found.</Placeholder>;
-    }
-
-    let tag = job.spec.template.spec.containers[0].image.split(":")[1];
-    return (
-      <ConfigSection>
-        {commandString ? (
-          <>
-            Command: <Command>{commandString}</Command>
-          </>
-        ) : (
-          <DarkMatter size="-18px" />
-        )}
-        <Row>
-          Image Tag: <Command>{tag}</Command>
-        </Row>
-        {!isEmpty(envObject) && (
-          <>
-            <KeyValueArray
-              envLoader={true}
-              values={envObject}
-              label="Environment variables:"
-              disabled={true}
-            />
-            <DarkMatter />
-          </>
-        )}
-      </ConfigSection>
-    );
-  };
-
-  const renderEventsSection = () => {
-    return (
-      <EventsTab
-        currentChart={currentChart}
-        overridingJobName={jobRun.metadata?.name}
-        setLogData={() => setCurrentTab("logs")}
-      />
-    );
-  };
-
-  const renderLogsSection = () => {
-    if (useDeprecatedLogs || !currentCluster.agent_integration_enabled) {
-      return (
-        <JobLogsWrapper>
-          <Logs
-            selectedPod={pods[0]}
-            podError={!pods[0] ? "Pod no longer exists." : ""}
-            rawText={true}
-          />
-        </JobLogsWrapper>
-      );
-    }
-
-    let initData: InitLogData = {};
-
-    if (run.status.completionTime) {
-      initData.timestamp = run.status.completionTime;
-    }
-
-    return (
-      <JobLogsWrapper>
-        <DeprecatedWarning>
-          Not seeing your logs? Switch back to{" "}
-          <DeprecatedSelect
-            onClick={() => {
-              setUseDeprecatedLogs(true);
-            }}
-          >
-            {" "}
-            deprecated logging.
-          </DeprecatedSelect>
-        </DeprecatedWarning>
-        <LogsSection
-          isFullscreen={false}
-          setIsFullscreen={() => { }}
-          overridingPodSelector={jobRun.metadata?.name}
-          currentChart={currentChart}
-          initData={initData}
-        />
-      </JobLogsWrapper>
-    );
-  };
-
-  if (isLoading) {
-    return <Loading />;
-  }
-
-  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 (
-    <StyledExpandedChart>
-      <BreadcrumbRow>
-        <Breadcrumb onClick={onClose}>
-          <ArrowIcon src={leftArrow} />
-          <Wrap>Back</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <HeaderWrapper>
-        <TitleSection icon={currentChart.chart.metadata.icon} iconWidth="33px">
-          {chart.name} <Gray>at {readableDate(run.status.startTime)}</Gray>
-        </TitleSection>
-        <InfoWrapper>
-          <LastDeployed>
-            {renderStatus(
-              false,
-              run,
-              pods[0],
-              run.status.completionTime
-                ? readableDate(run.status.completionTime)
-                : ""
-            )}
-            <TagWrapper>
-              Namespace <NamespaceTag>{currentProject?.capi_provisioner_enabled && chart.namespace.startsWith("porter-stack-") ? chart.namespace.replace("porter-stack-", "") : chart.namespace}</NamespaceTag>
-            </TagWrapper>
-            <DeploymentType currentChart={currentChart} />
-          </LastDeployed>
-        </InfoWrapper>
-      </HeaderWrapper>
-      <BodyWrapper>
-        <TabRegion
-          currentTab={currentTab}
-          setCurrentTab={(newTab: string) => {
-            setCurrentTab(newTab);
-          }}
-          options={options}
-        >
-          {currentTab === "events" && renderEventsSection()}
-          {currentTab === "logs" && renderLogsSection()}
-          {currentTab === "config" && <>{renderConfigSection(run)}</>}
-          {currentTab === "metrics" && (
-            <JobMetricsSection jobChart={currentChart} jobRun={run} />
-          )}
-        </TabRegion>
-      </BodyWrapper>
-    </StyledExpandedChart>
-  );
-};
-
-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;
-`;

+ 0 - 208
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx

@@ -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(
-        "<token>",
-        {},
-        {
-          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 (
-      <JobListWrapper>
-        <Placeholder height="350px">
-          <i className="material-icons">category</i>
-          There are no jobs currently running.
-        </Placeholder>
-      </JobListWrapper>
-    );
-  }
-
-  return (
-    <>
-      <JobListWrapper>
-        {props.jobs
-          .slice(firstContentIndex, lastContentIndex)
-          .map((job: any, i: number) => {
-            return (
-              <JobResource
-                key={job?.metadata?.name}
-                expandJob={props.expandJob}
-                job={job}
-                handleDelete={() => {
-                  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}
-              />
-            );
-          })}
-      </JobListWrapper>
-      <FlexEnd style={{ marginTop: "15px" }}>
-        {/* Disable the page count selector until find a fix for their styles */}
-        {/* <PageCountWrapper>
-          Page size:
-          <Selector
-            activeValue={String(pageSize)}
-            options={[
-              {
-                label: "10",
-                value: "10",
-              },
-              {
-                label: "20",
-                value: "20",
-              },
-              {
-                label: "50",
-                value: "50",
-              },
-              {
-                label: "100",
-                value: "100",
-              },
-            ]}
-            setActiveValue={(val) => setPageSize(Number(val))}
-            width="70px"
-          ></Selector>
-        </PageCountWrapper> */}
-        <PaginationActionsWrapper>
-          <PaginationAction disabled={!canPreviousPage} onClick={prevPage}>
-            {"<"}
-          </PaginationAction>
-          <PageCounter>
-            Page {page} of {totalPages}
-          </PageCounter>
-          <PaginationAction disabled={!canNextPage} onClick={nextPage}>
-            {">"}
-          </PaginationAction>
-        </PaginationActionsWrapper>
-      </FlexEnd>
-    </>
-  );
-};
-
-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;
-`;

+ 0 - 342
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx

@@ -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> = (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(
-        "<token>",
-        {},
-        {
-          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 (
-        <i className="material-icons" onClick={stopJob}>
-          stop
-        </i>
-      );
-    }
-
-    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 (
-        <DynamicLink
-          to={`https://github.com/${props.repositoryUrl}/commit/${tag}`}
-          onClick={(e) => e.preventDefault()}
-          target="_blank"
-        >
-          {tag}
-        </DynamicLink>
-      );
-    }
-
-    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 (
-      <RevisionContainer status={status}>
-        Revision No - {revision || "unknown"}
-      </RevisionContainer>
-    );
-  };
-
-  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 (
-    <>
-      <StyledJob>
-        <MainRow onClick={() => props.expandJob(props.job)}>
-          <Flex>
-            <Icon src={icon && icon} />
-            <Description>
-              <Label>
-                Started at {readableDate(props.job.status?.startTime)}
-                <Dot>•</Dot>
-                <span>
-                  {props.isDeployedFromGithub ? "Commit: " : "Image tag:"}{" "}
-                  {getImageTag()}
-                </span>
-              </Label>
-              <Subtitle>{getSubtitle()}</Subtitle>
-            </Description>
-          </Flex>
-          <EndWrapper>
-            <Flex>
-              {getRevisionNumber()}
-              <CommandString>{commandString}</CommandString>
-            </Flex>
-
-            {renderStatus(props.deleting, props.job, pods[0])}
-            <MaterialIconTray disabled={false}>
-              {renderStopButton()}
-              {!props.readOnly && (
-                <i
-                  className="material-icons"
-                  onClick={(e) => {
-                    e.stopPropagation();
-                    props.handleDelete();
-                  }}
-                >
-                  delete
-                </i>
-              )}
-            </MaterialIconTray>
-          </EndWrapper>
-        </MainRow>
-      </StyledJob>
-    </>
-  );
-};
-
-export default JobResource;
-
-type RevisionContainerProps = {
-  status: "outdated" | "current";
-};
-
-const RevisionContainer = styled.span<RevisionContainerProps>`
-  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;
-`;

+ 0 - 397
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/useJobs.ts

@@ -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(
-        "<token>",
-        {
-          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,
-  };
-};

+ 0 - 662
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx

@@ -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<React.SetStateAction<Date>>;
-  resetSearch: () => void;
-}
-
-const QueryModeSelectionToggle = (props: QueryModeSelectionToggleProps) => {
-  return (
-    <div
-      style={{
-        marginRight: "10px",
-        display: "flex",
-        gap: "10px",
-      }}
-    >
-      <ToggleButton>
-        <ToggleOption
-          onClick={() => {
-            props.setSelectedDate(undefined);
-            props.resetSearch();
-          }}
-          selected={!props.selectedDate}
-        >
-          <Dot selected={!props.selectedDate} />
-          Live
-        </ToggleOption>
-        <ToggleOption
-          nudgeLeft
-          onClick={() => { props.setSelectedDate(dayjs().toDate()); }}
-          selected={!!props.selectedDate}
-        >
-          <TimeIcon src={time} selected={!!props.selectedDate} />
-          {props.selectedDate && (
-            <DateTimePicker
-              startDate={props.selectedDate}
-              setStartDate={props.setSelectedDate}
-            />
-          )}
-        </ToggleOption>
-      </ToggleButton>
-    </div>
-  );
-};
-
-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<Props> = ({
-  currentChart,
-  isFullscreen,
-  setIsFullscreen,
-  initData = {},
-  setInitData,
-  overridingPodSelector,
-}) => {
-  const scrollToBottomRef = useRef<HTMLDivElement | undefined>(undefined);
-  const { currentProject, currentCluster } = useContext(Context);
-  const [scrollToBottomEnabled, setScrollToBottomEnabled] = useState(true);
-  const [searchText, setSearchText] = useState("");
-  const [enteredSearchText, setEnteredSearchText] = useState("");
-  const [selectedDate, setSelectedDate] = useState<Date | undefined>(
-    initData.timestamp ? dayjs(initData.timestamp).toDate() : undefined
-  );
-  const [notification, setNotification] = useState<string>();
-  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 key={[log.lineNumber, i].join(".")}>
-          <span className="line-number">{log.lineNumber}.</span>
-          <span className="line-timestamp">
-            {log.timestamp
-              ? dayjs(log.timestamp).format("MMM D, YYYY HH:mm:ss")
-              : "-"}
-          </span>
-          <LogOuter key={[log.lineNumber, i].join(".")}>
-            {log.line?.map((ansi, j) => {
-              if (ansi.clearLine) {
-                return null;
-              }
-
-              return (
-                <LogInnerSpan
-                  key={[log.lineNumber, i, j].join(".")}
-                  ansi={ansi}
-                >
-                  {ansi.content.replace(/ /g, "\u00a0")}
-                </LogInnerSpan>
-              );
-            })}
-          </LogOuter>
-        </Log>
-      );
-    });
-  };
-
-  const onLoadPrevious = useCallback(() => {
-    if (!selectedDate) {
-      setSelectedDate(dayjs(logs[0].timestamp).toDate());
-      return;
-    }
-
-    moveCursor(Direction.backward);
-  }, [logs, selectedDate]);
-
-  const resetSearch = () => {
-    setSearchText("");
-    setEnteredSearchText("");
-  };
-
-  const renderContents = () => {
-    return (
-      <>
-        <FlexRow isFullscreen={isFullscreen}>
-          <Flex>
-            <SearchRowWrapper>
-              <SearchBarWrapper>
-                <i className="material-icons">search</i>
-                <SearchInput
-                  value={searchText}
-                  onChange={(e: any) => {
-                    setSearchText(e.target.value);
-                  }}
-                  onKeyPress={(event) => {
-                    if (event.key === "Enter") {
-                      setEnteredSearchText(escapeExp(searchText));
-                      if (selectedDate == null) {
-                        setSelectedDate(dayjs().toDate());
-                      }
-                    }
-                  }}
-                  placeholder="Search logs..."
-                />
-              </SearchBarWrapper>
-            </SearchRowWrapper>
-            <QueryModeSelectionToggle
-              selectedDate={selectedDate}
-              setSelectedDate={setSelectedDate}
-              resetSearch={resetSearch}
-            />
-          </Flex>
-          <Flex>
-            <Button onClick={() => { setScrollToBottomEnabled((s) => !s); }}>
-              <Checkbox checked={scrollToBottomEnabled}>
-                <i className="material-icons">done</i>
-              </Checkbox>
-              Scroll to bottom
-            </Button>
-            <Spacer />
-            <Button onClick={refresh}>
-              <i className="material-icons">autorenew</i>
-              Refresh
-            </Button>
-            {!isFullscreen && (
-              <>
-                <Spacer />
-                <Icon onClick={() => { setIsFullscreen(true); }}>
-                  <i className="material-icons">open_in_full</i>
-                </Icon>
-              </>
-            )}
-          </Flex>
-        </FlexRow>
-        <LogsSectionWrapper>
-          <StyledLogsSection isFullscreen={isFullscreen}>
-            {loading || (logs.length == 0 && selectedDate == null) ? (
-              <Loading message="Waiting for logs..." />
-            ) : logs.length == 0 ? (
-              <>
-                <Message>
-                  No logs found.
-                  <Highlight onClick={refresh}>
-                    <i className="material-icons">autorenew</i>
-                    Refresh
-                  </Highlight>
-                </Message>
-              </>
-            ) : (
-              <>
-                <LoadMoreButton
-                  active={
-                    logs.length !== 0 && paginationInfo.previousCursor !== null
-                  }
-                  role="button"
-                  onClick={onLoadPrevious}
-                >
-                  Load Previous
-                </LoadMoreButton>
-                {renderLogs()}
-                <LoadMoreButton
-                  active={selectedDate && logs.length !== 0}
-                  role="button"
-                  onClick={async () => { await moveCursor(Direction.forward); }}
-                >
-                  Load more
-                </LoadMoreButton>
-              </>
-            )}
-            <div ref={scrollToBottomRef} />
-          </StyledLogsSection>
-          <NotificationWrapper
-            key={JSON.stringify(logs)}
-            active={!!notification}
-          >
-            <Banner>{notification}</Banner>
-          </NotificationWrapper>
-        </LogsSectionWrapper>
-      </>
-    );
-  };
-
-  return (
-    <>
-      {isFullscreen ? (
-        <Fullscreen>
-          <AbsoluteTitle>
-            <BackButton onClick={() => { setIsFullscreen(false); }}>
-              <i className="material-icons">navigate_before</i>
-            </BackButton>
-            Logs ({currentChart.name})
-          </AbsoluteTitle>
-          {renderContents()}
-        </Fullscreen>
-      ) : (
-        <>{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;
-`;

+ 0 - 436
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs.ts

@@ -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<typeof LogSchema>;
-
-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<Log[]>([]);
-  const { currentCluster, currentProject, setCurrentError } = useContext(
-    Context
-  );
-  const [logs, setLogs] = useState<Log[]>([]);
-  const [paginationInfo, setPaginationInfo] = useState<PaginationInfo>({
-    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(
-        "<token>",
-        {
-          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,
-  };
-};

+ 0 - 497
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx

@@ -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<NormalizedMetricsData, Date>(
-  (d) => new Date(d.date * 1000)
-).left;
-
-export type AreaProps = {
-  data: NormalizedMetricsData[];
-  aggregatedData?: Record<string, NormalizedMetricsData[]>;
-  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<AreaProps> = ({
-  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<string, NormalizedMetricsData>;
-  }>();
-
-  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<string, NormalizedMetricsData> = {};
-    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<SVGRectElement> | React.MouseEvent<SVGRectElement>
-    ) => {
-      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 (
-    <div>
-      <svg width={width} height={height} ref={svgContainer}>
-        <rect
-          x={0}
-          y={0}
-          width={width}
-          height={height}
-          fill="url(#area-background-gradient)"
-          rx={14}
-        />
-
-        <LinearGradient
-          id="area-background-gradient"
-          from={background}
-          to={background2}
-        />
-        <LinearGradient
-          id="area-gradient"
-          from={accentColor}
-          to={accentColor}
-          toOpacity={0}
-        />
-        {Object.entries(AggregatedDataColors).map(([dataKey, color]) => (
-          <LinearGradient
-            key={dataKey}
-            id={`area-gradient-${dataKey}`}
-            from={color}
-            to={color}
-            toOpacity={0}
-          />
-        ))}
-        <GridRows
-          left={margin.left}
-          scale={valueScale}
-          width={innerWidth}
-          strokeDasharray="1,3"
-          stroke="white"
-          strokeOpacity={0.2}
-          pointerEvents="none"
-        />
-        <GridColumns
-          top={margin.top}
-          scale={dateScale}
-          height={innerHeight}
-          strokeDasharray="1,3"
-          stroke="white"
-          strokeOpacity={0.2}
-          pointerEvents="none"
-        />
-        <AreaClosed<NormalizedMetricsData>
-          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]) => (
-          <LinePath<NormalizedMetricsData>
-            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 && (
-          <LinePath<NormalizedMetricsData>
-            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"
-          />
-        )}
-
-        <AxisLeft
-          left={10}
-          scale={valueScale}
-          hideAxisLine={true}
-          hideTicks={true}
-          tickLabelProps={() => ({
-            fill: "white",
-            fontSize: 11,
-            textAnchor: "start",
-            fillOpacity: 0.4,
-            dy: 0,
-          })}
-        />
-        <AxisBottom
-          top={height - 20}
-          scale={dateScale}
-          tickFormat={formats[resolution]}
-          hideAxisLine={true}
-          hideTicks={true}
-          tickLabelProps={() => ({
-            fill: "white",
-            fontSize: 11,
-            textAnchor: "middle",
-            fillOpacity: 0.4,
-          })}
-        />
-        <Bar
-          x={margin.left}
-          y={margin.top}
-          width={innerWidth}
-          height={innerHeight}
-          fill="transparent"
-          rx={14}
-          onTouchStart={handleTooltip}
-          onTouchMove={handleTooltip}
-          onMouseMove={handleTooltip}
-          onMouseLeave={() => hideTooltip()}
-        />
-        {tooltipData && (
-          <g>
-            <Line
-              from={{ x: tooltipLeft, y: margin.top }}
-              to={{ x: tooltipLeft, y: innerHeight + margin.top }}
-              stroke={accentColorDark}
-              strokeWidth={2}
-              pointerEvents="none"
-              strokeDasharray="5,2"
-            />
-            <circle
-              cx={tooltipLeft}
-              cy={dataGraphTooltipGlyphPosition + 1}
-              r={4}
-              fill="black"
-              fillOpacity={0.1}
-              stroke="black"
-              strokeOpacity={0.1}
-              strokeWidth={2}
-              pointerEvents="none"
-            />
-            {Object.entries(tooltipData.aggregatedData).map(([key, d]) => (
-              <circle
-                key={key}
-                cx={tooltipLeft}
-                cy={valueScale(getValue(d)) + 1}
-                r={4}
-                fill="black"
-                fillOpacity={0.1}
-                stroke="black"
-                strokeOpacity={0.1}
-                strokeWidth={2}
-                pointerEvents="none"
-              />
-            ))}
-            <circle
-              cx={tooltipLeft}
-              cy={dataGraphTooltipGlyphPosition}
-              r={4}
-              fill={accentColorDark}
-              stroke="white"
-              strokeWidth={2}
-              pointerEvents="none"
-            />
-            {Object.entries(tooltipData.aggregatedData).map(([key, d]) => (
-              <circle
-                key={key}
-                cx={tooltipLeft}
-                cy={valueScale(getValue(d))}
-                r={4}
-                fill={accentColorDark}
-                stroke="white"
-                strokeWidth={2}
-                pointerEvents="none"
-              />
-            ))}
-            {isHpaEnabled && hpaGraphTooltipGlyphPosition !== null && (
-              <>
-                <circle
-                  cx={tooltipLeft}
-                  cy={hpaGraphTooltipGlyphPosition + 1}
-                  r={4}
-                  fill="black"
-                  fillOpacity={0.1}
-                  stroke="black"
-                  strokeOpacity={0.1}
-                  strokeWidth={2}
-                  pointerEvents="none"
-                />
-                <circle
-                  cx={tooltipLeft}
-                  cy={hpaGraphTooltipGlyphPosition}
-                  r={4}
-                  fill={accentColorDark}
-                  stroke="white"
-                  strokeWidth={2}
-                  pointerEvents="none"
-                />
-              </>
-            )}
-          </g>
-        )}
-      </svg>
-      {tooltipData && (
-        <div>
-          <TooltipWithBounds
-            key={Math.random()}
-            top={tooltipTop - 12}
-            left={tooltipLeft + 12}
-            style={{
-              ...defaultStyles,
-              background: "#26272f",
-              color: "#aaaabb",
-            }}
-          >
-            <TooltipDate>{formatDate(getDate(tooltipData.data))}</TooltipDate>
-            <TooltipDataRow>
-              {dataKey}: {getValue(tooltipData.data)}
-            </TooltipDataRow>
-            {Object.entries(tooltipData.aggregatedData).map(([key, value]) => (
-              <TooltipDataRow color={AggregatedDataColors[key]} key={key}>
-                {`${key.toUpperCase()}. ${dataKey}`}: {getValue(value)}
-              </TooltipDataRow>
-            ))}
-            {isHpaEnabled && hpaGraphTooltipGlyphPosition !== null && (
-              <div style={{ color: "#FFF" }}>
-                Autoscaling Threshold: {getValue(tooltipData.tooltipHpaData)}
-              </div>
-            )}
-          </TooltipWithBounds>
-        </div>
-      )}
-    </div>
-  );
-};
-
-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;
-`;

+ 0 - 561
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/JobMetricsSection.tsx

@@ -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<PropsType> = ({
-  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<NormalizedMetricsData[]>([]);
-  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(
-        "<token>",
-        {},
-        {
-          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(
-        "<token>",
-        {
-          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(
-        "<token>",
-        {
-          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 (
-          <>
-            <DropdownOverlay onClick={() => setShowMetricsSettings(false)} />
-            <DropdownAlt dropdownWidth="330px" dropdownMaxHeight="300px">
-              <Label>Additional Settings</Label>
-              <SelectRow
-                label="Target Ingress"
-                value={selectedIngress}
-                setActiveValue={(x: any) => setSelectedIngress(x)}
-                options={ingressOptions}
-                width="100%"
-              />
-            </DropdownAlt>
-          </>
-        );
-      }
-
-      return (
-        <>
-          <DropdownOverlay onClick={() => setShowMetricsSettings(false)} />
-          <DropdownAlt dropdownWidth="330px" dropdownMaxHeight="300px">
-            <Label>Additional Settings</Label>
-            <SelectRow
-              label="Target Controller"
-              value={selectedController}
-              setActiveValue={(x: any) => setSelectedController(x)}
-              options={controllerOptions}
-              width="100%"
-            />
-          </DropdownAlt>
-        </>
-      );
-    }
-  };
-
-  const renderDropdown = () => {
-    if (dropdownExpanded) {
-      return (
-        <>
-          <DropdownOverlay onClick={() => setDropdownExpanded(false)} />
-          <Dropdown
-            dropdownWidth="230px"
-            dropdownMaxHeight="200px"
-            onClick={() => setDropdownExpanded(false)}
-          >
-            {renderOptionList()}
-          </Dropdown>
-        </>
-      );
-    }
-  };
-
-  const renderOptionList = () => {
-    return metricsOptions.map(
-      (option: { value: string; label: string }, i: number) => {
-        return (
-          <Option
-            key={i}
-            selected={option.value === selectedMetric}
-            onClick={() => {
-              setSelectedMetric(option.value);
-              setSelectedMetricLabel(option.label);
-            }}
-            lastItem={i === metricsOptions.length - 1}
-          >
-            {option.label}
-          </Option>
-        );
-      }
-    );
-  };
-
-  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 (
-    <StyledMetricsSection>
-      <MetricsHeader>
-        <Flex>
-          <MetricSelector
-            onClick={() => setDropdownExpanded(!dropdownExpanded)}
-          >
-            <MetricsLabel>{selectedMetricLabel}</MetricsLabel>
-            <i className="material-icons">arrow_drop_down</i>
-            {renderDropdown()}
-          </MetricSelector>
-
-          <Highlight color={"#7d7d81"} onClick={getMetrics}>
-            <i className="material-icons">autorenew</i>
-          </Highlight>
-        </Flex>
-      </MetricsHeader>
-      {isLoading > 0 && <Loading />}
-      {data.length === 0 && isLoading === 0 && (
-        <>
-          {selectedMetric === "cpu" && hasJobRunnedForMoreThan5m() ? (
-            <Message>
-              No data available yet.
-              <Highlight color={"#8590ff"} onClick={getMetrics}>
-                <i className="material-icons">autorenew</i>
-                Refresh
-              </Highlight>
-            </Message>
-          ) : (
-            <Message>
-              <Highlight color={"#8590ff"} disableHover>
-                CPU data is not available for jobs that ran for less than 5
-                minutes.
-              </Highlight>
-            </Message>
-          )}
-        </>
-      )}
-      {data.length > 0 && isLoading === 0 && (
-        <>
-          {currentChart?.config?.autoscaling?.enabled &&
-            ["cpu", "memory"].includes(selectedMetric) && (
-              <CheckboxRow
-                toggle={() => setHpaEnabled((prev: any) => !prev)}
-                checked={hpaEnabled}
-                label="Show Autoscaling Threshold"
-              />
-            )}
-          <ParentSize>
-            {({ width, height }) => (
-              <AreaChart
-                dataKey={selectedMetricLabel}
-                data={data}
-                hpaData={hpaData}
-                hpaEnabled={
-                  hpaEnabled && ["cpu", "memory"].includes(selectedMetric)
-                }
-                width={width}
-                height={height - 10}
-                resolution={selectedRange}
-                margin={{ top: 40, right: -40, bottom: 0, left: 50 }}
-              />
-            )}
-          </ParentSize>
-        </>
-      )}
-    </StyledMetricsSection>
-  );
-};
-
-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);
-    }
-  }
-`;

+ 0 - 174
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts

@@ -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<string, NormalizedMetricsData[]> {
-    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),
-      };
-    });
-  }
-}

+ 0 - 750
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx

@@ -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<PropsType> = ({
-  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<NormalizedMetricsData[]>([]);
-  const [aggregatedData, setAggregatedData] = useState<
-    Record<string, NormalizedMetricsData[]>
-  >({});
-  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(
-          "<token>",
-          {},
-          {
-            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(
-        "<token>",
-        {},
-        {
-          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(
-        "<token>",
-        {
-          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(
-        "<token>",
-        {
-          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(
-        "<token>",
-        {
-          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(
-        "<token>",
-        {
-          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 (
-          <>
-            <DropdownOverlay onClick={() => setShowMetricsSettings(false)} />
-            <DropdownAlt dropdownWidth="330px" dropdownMaxHeight="300px">
-              <Label>Additional Settings</Label>
-              <SelectRow
-                label="Target Ingress"
-                value={selectedIngress}
-                setActiveValue={(x: any) => setSelectedIngress(x)}
-                options={ingressOptions}
-                width="100%"
-              />
-            </DropdownAlt>
-          </>
-        );
-      }
-
-      return (
-        <>
-          <DropdownOverlay onClick={() => setShowMetricsSettings(false)} />
-          <DropdownAlt dropdownWidth="330px" dropdownMaxHeight="300px">
-            <Label>Additional Settings</Label>
-            <SelectRow
-              label="Target Controller"
-              value={selectedController}
-              setActiveValue={(x: any) => setSelectedController(x)}
-              options={controllerOptions}
-              width="100%"
-            />
-            <SelectRow
-              label="Target Pod"
-              value={selectedPod}
-              setActiveValue={(x: any) => setSelectedPod(x)}
-              options={pods}
-              width="100%"
-            />
-          </DropdownAlt>
-        </>
-      );
-    }
-  };
-
-  const renderDropdown = () => {
-    if (dropdownExpanded) {
-      return (
-        <>
-          <DropdownOverlay onClick={() => setDropdownExpanded(false)} />
-          <Dropdown
-            dropdownWidth="230px"
-            dropdownMaxHeight="200px"
-            onClick={() => setDropdownExpanded(false)}
-          >
-            {renderOptionList()}
-          </Dropdown>
-        </>
-      );
-    }
-  };
-
-  const renderOptionList = () => {
-    return metricsOptions.map(
-      (option: { value: string; label: string }, i: number) => {
-        return (
-          <Option
-            key={i}
-            selected={option.value === selectedMetric}
-            onClick={() => {
-              setSelectedMetric(option.value);
-              setSelectedMetricLabel(option.label);
-            }}
-            lastItem={i === metricsOptions.length - 1}
-          >
-            {option.label}
-          </Option>
-        );
-      }
-    );
-  };
-
-  return (
-    <StyledMetricsSection>
-      <MetricsHeader>
-        <Flex>
-          <MetricSelector
-            onClick={() => setDropdownExpanded(!dropdownExpanded)}
-          >
-            <MetricsLabel>{selectedMetricLabel}</MetricsLabel>
-            <i className="material-icons">arrow_drop_down</i>
-            {renderDropdown()}
-          </MetricSelector>
-          <Relative>
-            <IconWrapper onClick={() => setShowMetricsSettings(true)}>
-              <SettingsIcon src={settings} />
-            </IconWrapper>
-            {renderMetricsSettings()}
-          </Relative>
-
-          <Highlight color={"#7d7d81"} onClick={getMetrics}>
-            <i className="material-icons">autorenew</i>
-          </Highlight>
-        </Flex>
-        <RangeWrapper>
-          <TabSelector
-            noBuffer={true}
-            options={[
-              { value: "1H", label: "1H" },
-              { value: "6H", label: "6H" },
-              { value: "1D", label: "1D" },
-              { value: "1M", label: "1M" },
-            ]}
-            currentTab={selectedRange}
-            setCurrentTab={(x: string) => setSelectedRange(x)}
-          />
-        </RangeWrapper>
-      </MetricsHeader>
-      {isLoading > 0 && <Loading />}
-      {data.length === 0 && isLoading === 0 && (
-        <Message>
-          No data available yet.
-          <Highlight color={"#8590ff"} onClick={getMetrics}>
-            <i className="material-icons">autorenew</i>
-            Refresh
-          </Highlight>
-        </Message>
-      )}
-      {data.length > 0 && isLoading === 0 && (
-        <>
-          <AggregatedDataLegend data={data} hideAvg={false} />
-          {currentChart?.config?.autoscaling?.enabled &&
-            ["cpu", "memory"].includes(selectedMetric) && (
-              <CheckboxRow
-                toggle={() => setHpaEnabled((prev: any) => !prev)}
-                checked={hpaEnabled}
-                label="Show Autoscaling Threshold"
-              />
-            )}
-          <ParentSize>
-            {({ width, height }) => (
-              <AreaChart
-                dataKey={selectedMetricLabel}
-                aggregatedData={aggregatedData}
-                data={data}
-                hpaData={hpaData}
-                hpaEnabled={
-                  hpaEnabled && ["cpu", "memory"].includes(selectedMetric)
-                }
-                width={width}
-                height={height - 10}
-                resolution={selectedRange}
-                margin={{ top: 40, right: -40, bottom: 0, left: 50 }}
-              />
-            )}
-          </ParentSize>
-        </>
-      )}
-    </StyledMetricsSection>
-  );
-};
-
-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);
-    }
-  }
-`;

+ 0 - 439
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx

@@ -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<Props> = ({
-  controller,
-  selectPod,
-  isFirst,
-  isLast,
-  selectors,
-  setPodError,
-  selectedPod,
-}) => {
-  const [pods, setPods] = useState<ControllerTabPodType[]>([]);
-  const [rawPodList, setRawPodList] = useState<any[]>([]);
-  const [podPendingDelete, setPodPendingDelete] = useState<any>(null);
-  const [available, setAvailable] = useState<number>(null);
-  const [total, setTotal] = useState<number>(null);
-  const [userSelectedPod, setUserSelectedPod] = useState<boolean>(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(
-        "<token>",
-        {
-          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<ControllerTabPodType>((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(
-        "<token>",
-        {},
-        {
-          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<Array<ControllerTabPodType>>
-    >(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 (
-        <PodRow
-          key={i}
-          pod={pod}
-          isSelected={currentSelectedPod?.name === pod?.name}
-          podStatus={status}
-          isLastItem={i === arr.length - 1}
-          onTabClick={() => {
-            setPodError("");
-            status === "failed" &&
-              pod.status?.message &&
-              setPodError(pod.status?.message);
-            handleSelectPod(pod);
-            setUserSelectedPod(true);
-          }}
-          onDeleteClick={() => setPodPendingDelete(pod)}
-        />
-      );
-    });
-  };
-
-  return (
-    <ResourceTab
-      label={controller.kind}
-      // handle CronJob case
-      name={controller.metadata?.name || controller.name}
-      status={{ label: currentControllerStatus, available, total }}
-      isLast={isLast}
-      expanded={isFirst}
-    >
-      {!!replicaSetArray.length &&
-        replicaSetArray.map((subArray, index) => {
-          const firstItem = subArray[0];
-          return (
-            <div key={firstItem.replicaSetName + index}>
-              <ReplicaSetContainer>
-                <ReplicaSetName>
-                  {firstItem?.revisionNumber &&
-                    firstItem?.revisionNumber.toString() != "N/A" && (
-                      <Bold>Revision {firstItem.revisionNumber}:</Bold>
-                    )}{" "}
-                  {firstItem.replicaSetName}
-                </ReplicaSetName>
-              </ReplicaSetContainer>
-              {mapPods(subArray)}
-            </div>
-          );
-        })}
-      {!replicaSetArray.length && mapPods(pods)}
-      <ConfirmOverlay
-        message="Are you sure you want to delete this pod?"
-        show={podPendingDelete}
-        onYes={() => handleDeletePod(podPendingDelete)}
-        onNo={() => setPodPendingDelete(null)}
-      />
-    </ResourceTab>
-  );
-};
-
-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;
-`;

+ 0 - 398
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -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<boolean>(true);
-  const wrapperRef = useRef<HTMLDivElement>();
-
-  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<boolean>(false);
-
-  useEffect(() => {
-    shouldScroll.current = isScrollToBottomEnabled;
-  }, [isScrollToBottomEnabled]);
-
-  const renderLogs = () => {
-    if (podError && podError != "") {
-      return <Message>{podError}</Message>;
-    }
-
-    if (!selectedPod?.metadata?.name) {
-      return <Message>Please select a pod to view its logs.</Message>;
-    }
-
-    if (selectedPod?.status.phase === "Succeeded" && !rawText) {
-      return (
-        <Message>
-          ⌛ This job has been completed. You can now delete this job.
-        </Message>
-      );
-    }
-
-    if (
-      showPreviousLogs &&
-      Array.isArray(previousLogs) &&
-      previousLogs.length
-    ) {
-      return previousLogs?.map((log, i) => {
-        return (
-          <Log key={i}>
-            {log.map((ansi, j) => {
-              if (ansi.clearLine) {
-                return null;
-              }
-
-              return (
-                <LogSpan key={i + "." + j} ansi={ansi}>
-                  {ansi.content.replace(/ /g, "\u00a0")}
-                </LogSpan>
-              );
-            })}
-          </Log>
-        );
-      });
-    }
-
-    if (!Array.isArray(logs) || logs?.length === 0) {
-      return (
-        <Message>
-          No logs to display from this pod.
-          <Highlight onClick={refresh}>
-            <i className="material-icons">autorenew</i>
-            Refresh
-          </Highlight>
-        </Message>
-      );
-    }
-
-    return logs?.map((log, i) => {
-      return (
-        <Log key={i}>
-          {log.map((ansi, j) => {
-            if (ansi.clearLine) {
-              return null;
-            }
-
-            return (
-              <LogSpan key={i + "." + j} ansi={ansi}>
-                {ansi.content.replace(/ /g, "\u00a0")}
-              </LogSpan>
-            );
-          })}
-        </Log>
-      );
-    });
-  };
-
-  const renderContent = () => (
-    <>
-      {/* <ConnectToLogsInstructionModal
-        show={showConnectionModal}
-        onClose={() => setShowConnectionModal(false)}
-        chartName={selectedPod?.metadata?.labels["app.kubernetes.io/instance"]}
-        namespace={selectedPod?.metadata?.namespace}
-      />
-      <CLIModalIconWrapper
-        onClick={(e) => {
-          e.preventDefault();
-          setShowConnectionModal(true);
-        }}
-      >
-        <CLIModalIcon />
-        CLI Logs Instructions
-      </CLIModalIconWrapper> */}
-      <Wrapper ref={wrapperRef}>{renderLogs()}</Wrapper>
-      <LogTabs>
-        {containers.map((containerName, _i, arr) => {
-          return (
-            <Tab
-              key={containerName}
-              onClick={() => {
-                setCurrentContainer(containerName);
-              }}
-              clicked={currentContainer === containerName}
-            >
-              {arr.length > 1 ? containerName : "Application"}
-            </Tab>
-          );
-        })}
-        <Tab
-          onClick={() => {
-            setCurrentContainer("system");
-          }}
-          clicked={currentContainer == "system"}
-        >
-          System
-        </Tab>
-      </LogTabs>
-      <Options>
-        <Scroll
-          onClick={() => {
-            setIsScrollToBottomEnabled(!isScrollToBottomEnabled);
-            if (isScrollToBottomEnabled) {
-              scrollToBottom(true);
-            }
-          }}
-        >
-          <input
-            type="checkbox"
-            checked={isScrollToBottomEnabled}
-            onChange={() => {}}
-          />
-          Scroll to bottom
-        </Scroll>
-        {Array.isArray(previousLogs) && previousLogs.length > 0 && (
-          <Scroll
-            onClick={() => {
-              setShowPreviousLogs(!showPreviousLogs);
-            }}
-          >
-            <input
-              type="checkbox"
-              checked={showPreviousLogs}
-              onChange={() => {}}
-            />
-            Show previous logs
-          </Scroll>
-        )}
-        <Refresh onClick={() => refresh()}>
-          <i className="material-icons">autorenew</i>
-          Refresh
-        </Refresh>
-      </Options>
-    </>
-  );
-
-  if (!containers?.length) {
-    return null;
-  }
-
-  if (rawText) {
-    return <LogStreamAlt>{renderContent()}</LogStreamAlt>;
-  }
-
-  return <LogStream>{renderContent()}</LogStream>;
-};
-
-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;
-  }
-`;

+ 0 - 234
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/PodRow.tsx

@@ -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<PodRowProps> = ({
-  pod,
-  isSelected,
-  onTabClick,
-  onDeleteClick,
-  isLastItem,
-  podStatus,
-}) => {
-  const [showTooltip, setShowTooltip] = useState(false);
-
-  return (
-    <Tab key={pod?.name} selected={isSelected} onClick={onTabClick}>
-      <Gutter>
-        <Rail />
-        <Circle />
-        <Rail lastTab={isLastItem} />
-      </Gutter>
-      <Name
-        onMouseOver={() => {
-          setShowTooltip(true);
-        }}
-        onMouseOut={() => {
-          setShowTooltip(false);
-        }}
-      >
-        {pod?.name}
-      </Name>
-      {showTooltip && (
-        <Tooltip>
-          {pod?.name}
-          <Grey>Restart count: {pod.restartCount}</Grey>
-          <Grey>Created on: {pod.podAge}</Grey>
-          {podStatus === "failed" ? (
-            <FailedStatusContainer>
-              <Grey>
-                Failure Reason: {pod?.containerStatus?.state?.waiting?.reason}
-              </Grey>
-              <Grey>{pod?.containerStatus?.state?.waiting?.message}</Grey>
-            </FailedStatusContainer>
-          ) : null}
-        </Tooltip>
-      )}
-
-      <Status>
-        <StatusColor status={podStatus} />
-        {podStatus}
-        {podStatus === "failed" && (
-          <CloseIcon
-            className="material-icons-outlined"
-            onClick={onDeleteClick}
-          >
-            close
-          </CloseIcon>
-        )}
-      </Status>
-    </Tab>
-  );
-};
-
-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;
-`;

+ 0 - 288
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx

@@ -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<Props> = ({
-  currentChart,
-  fullscreen,
-  setFullScreenLogs,
-  selectors,
-}) => {
-  const [selectedPod, setSelectedPod] = useState<any>({});
-  const [controllers, setControllers] = useState<any[]>([]);
-  const [isLoading, setIsLoading] = useState<boolean>(true);
-  const [podError, setPodError] = useState<string>("");
-
-  const { currentProject, currentCluster, setCurrentError } = useContext(
-    Context
-  );
-
-  useEffect(() => {
-    let isSubscribed = true;
-    api
-      .getChartControllers(
-        "<token>",
-        {},
-        {
-          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 (
-      <Logs
-        podError={podError}
-        key={selectedPod?.metadata?.name}
-        selectedPod={selectedPod}
-      />
-    );
-  };
-
-  const renderTabs = () => {
-    return controllers.map((c, i) => {
-      return (
-        <ControllerTab
-          // handle CronJob case
-          key={c.metadata?.uid || c.uid}
-          selectedPod={selectedPod}
-          selectPod={setSelectedPod}
-          selectors={selectors ? [selectors[i]] : null}
-          controller={c}
-          isLast={i === controllers?.length - 1}
-          isFirst={i === 0}
-          setPodError={(x: string) => setPodError(x)}
-        />
-      );
-    });
-  };
-
-  const renderStatusSection = () => {
-    if (isLoading) {
-      return (
-        <NoControllers>
-          <Loading />
-        </NoControllers>
-      );
-    }
-    if (controllers?.length > 0) {
-      return (
-        <Wrapper>
-          <TabWrapper>{renderTabs()}</TabWrapper>
-          {renderLogs()}
-        </Wrapper>
-      );
-    }
-
-    if (currentChart?.chart?.metadata?.name === "job") {
-      return (
-        <NoControllers>
-          <i className="material-icons">category</i>
-          There are no jobs currently running.
-        </NoControllers>
-      );
-    }
-
-    return (
-      <NoControllers>
-        <i className="material-icons">category</i>
-        No objects to display. This might happen while your app is still
-        deploying.
-      </NoControllers>
-    );
-  };
-
-  return (
-    <>
-      {fullscreen ? (
-        <FullScreen>
-          <AbsoluteTitle>
-            <BackButton onClick={setFullScreenLogs}>
-              <i className="material-icons">navigate_before</i>
-            </BackButton>
-            Status ({currentChart.name})
-          </AbsoluteTitle>
-          <FullScreenButton top="70px" onClick={setFullScreenLogs}>
-            <i className="material-icons">close_fullscreen</i>
-          </FullScreenButton>
-          {renderStatusSection()}
-        </FullScreen>
-      ) : (
-        <StyledStatusSection>
-          <FullScreenButton onClick={setFullScreenLogs}>
-            <i className="material-icons">open_in_full</i>
-          </FullScreenButton>
-          {renderStatusSection()}
-        </StyledStatusSection>
-      )}
-    </>
-  );
-};
-
-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;
-  }
-`;

+ 0 - 19
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/types.ts

@@ -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;
-  };
-};

+ 0 - 218
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/useLogs.ts

@@ -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<string>();
-
-  const { currentCluster, currentProject } = useContext(Context);
-  const [containers, setContainers] = useState<string[]>([]);
-  const [currentContainer, setCurrentContainer] = useState<string>("");
-  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(
-        "<token>",
-        {},
-        {
-          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[] }>(
-          "<token>",
-          {
-            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,
-  };
-};

+ 0 - 79
dashboard/src/main/home/cluster-dashboard/expanded-chart/useStackEnvGroups.ts

@@ -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<PopulatedEnvGroup>(
-          "<token>",
-          {},
-          {
-            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<Stack>(
-        "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,
-  };
-};

+ 0 - 226
dashboard/src/main/home/cluster-dashboard/jobs/JobDashboard.tsx

@@ -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<Props> = ({
-  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 (
-    <StyledJobDashboard>
-      <DashboardHeader
-        image={job}
-        title={currentView}
-        description="Scripts and tasks that run once or on a repeating interval."
-        disableLineBreak
-      />
-      {currentCluster.status === "UPDATING_UNAVAILABLE" ? (
-        <ClusterProvisioningPlaceholder />
-      ) : (
-        <>
-          <ControlRow>
-            <FilterWrapper>
-              <LastRunStatusSelector
-                lastRunStatus={lastRunStatus}
-                setLastRunStatus={setLastRunStatus}
-              />
-              <NamespaceSelector
-                setNamespace={(x) => {
-                  setNamespace(x);
-                  pushQueryParams(props, {
-                    namespace: x || "ALL",
-                  });
-                }}
-                namespace={namespace}
-              />
-              <TagFilter
-                onSelect={setSelectedTag}
-              />
-            </FilterWrapper>
-            <Flex>
-              <ToggleButton>
-                <ToggleOption
-                  onClick={() => setShowRuns(false)}
-                  selected={!showRuns}
-                >
-                  Jobs
-                </ToggleOption>
-                <ToggleOption
-                  nudgeLeft
-                  onClick={() => setShowRuns(true)}
-                  selected={showRuns}
-                >
-                  Runs
-                </ToggleOption>
-              </ToggleButton>
-              {props.isAuthorized(
-                "namespace",
-                [],
-                ["get", "create"]
-              ) && (
-                  <Button
-                    onClick={() => {
-                      pushFiltered(props, "/launch", ["project_id"]);
-                    }}
-                  >
-                    <i className="material-icons">add</i> Launch template
-                  </Button>
-                )}
-            </Flex>
-          </ControlRow>
-          <HidableElement show={showRuns}>
-            <JobRunTable
-              lastRunStatus={lastRunStatus}
-              namespace={namespace}
-              sortType={sortType}
-            />
-          </HidableElement>
-          <HidableElement show={!showRuns}>
-            <ChartList
-              currentView={currentView}
-              currentCluster={currentCluster}
-              lastRunStatus={lastRunStatus}
-              namespace={currentProject?.capi_provisioner_enabled ? "default" : namespace}
-              sortType={sortType}
-              selectedTag={selectedTag}
-            />
-          </HidableElement>
-        </>
-      )}
-    </StyledJobDashboard>
-  );
-};
-
-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")};
-`;

+ 0 - 617
dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx

@@ -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<string[]>([]);
-
-  const [status, setStatus] = useState(null);
-  const { pushFiltered } = useRouting();
-  const [showSettings, setShowSettings] = useState<boolean>(false);
-  const [currentStep, setCurrentStep] = useState<number>(0);
-
-  // NOTE: git_repo_id is a misnomer as this actually refers to the github app's installation id.
-  const [actionConfig, setActionConfig] = useState<ActionConfigType>({
-    git_repo: null,
-    image_repo_uri: null,
-    git_branch: null,
-    git_repo_id: 0,
-    kind: "github",
-  });
-
-  // Branch selector data
-  const [baseBranches, setBaseBranches] = useState<string[]>([]);
-  const [deployBranches, setDeployBranches] = useState<string[]>([]);
-  const [availableBranches, setAvailableBranches] = useState<string[]>([]);
-  const [isLoadingBranches, setIsLoadingBranches] = useState(false);
-
-  // Disable new comments data
-  const [isNewCommentsDisabled, setIsNewCommentsDisabled] = useState(false);
-
-  // Namespace labels
-  const [namespaceLabels, setNamespaceLabels] = useState<KeyValueType[]>([]);
-
-  useEffect(() => {
-    api
-      .listEnvironments<Environment[]>(
-        "<token>",
-        {},
-        {
-          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<string[]>(
-        "<token>",
-        {},
-        {
-          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<string, string> = {};
-
-    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(
-        "<token>",
-        {
-          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 (
-      <CenterWrapper>
-        <Div>
-          <Back to="/preview-environments" />
-          <DashboardHeader
-            image={PullRequestIcon}
-            title="Preview environments"
-            capitalize={false}
-            description="Create full-stack preview environments for your pull requests."
-          />
-          <VerticalSteps
-            currentStep={currentStep}
-            steps={[
-              <>
-                <Text size={16}>Choose a repository</Text>
-                <ConnectNewRepoActionConfEditor
-                  actionConfig={actionConfig}
-                  setActionConfig={(actionConfig: ActionConfigType) => {
-                    setActionConfig(
-                      (currentActionConfig: ActionConfigType) => ({
-                        ...currentActionConfig,
-                        ...actionConfig,
-                      })
-                    );
-
-                    if (!!actionConfig.git_repo) {
-                      setCurrentStep((prev) => {
-                        if (prev > 0) {
-                          return prev;
-                        }
-
-                        return prev + 1;
-                      });
-                    }
-                  }}
-                />
-                <HelperContainer>
-                  Note: you will need to add a{" "}
-                  <CodeBlock>porter.yaml</CodeBlock> file to create a preview
-                  environment.
-                  <DocsHelper
-                    disableMargin
-                    tooltipText="A Porter YAML file is a declarative set of resources that Porter uses to build and update your preview environment deployments."
-                    link="https://docs.porter.run/preview-environments/porter-yaml-reference"
-                  />
-                </HelperContainer>
-              </>,
-
-              <>
-                <Text size={16}>Automatic pull request deployments</Text>
-                <Helper style={{ marginTop: "10px", marginBottom: "10px" }}>
-                  If you enable this option, the new pull requests will be
-                  automatically deployed.
-                </Helper>
-                <CheckboxWrapper>
-                  <CheckboxRow
-                    label="Enable automatic deploys"
-                    checked={enableAutomaticDeployments}
-                    toggle={() => {
-                      setEnableAutomaticDeployments(
-                        !enableAutomaticDeployments
-                      );
-                    }}
-                    wrapperStyles={{
-                      disableMargin: true,
-                    }}
-                  />
-                </CheckboxWrapper>
-              </>,
-            ]}
-          />
-          <ActionContainer>
-            <SaveButton
-              text="Add repository"
-              disabled={actionConfig.git_repo_id ? false : true}
-              onClick={addRepo}
-              makeFlush={true}
-              clearPosition={true}
-              status={status}
-              statusPosition={"left"}
-            />
-          </ActionContainer>
-        </Div>
-      </CenterWrapper>
-    );
-  }
-
-  return (
-    <>
-      <DashboardHeader
-        image={PullRequestIcon}
-        title="Preview environments"
-        capitalize={false}
-        description="Create full-stack preview environments for your pull requests."
-      />
-
-      <HeaderSection>
-        <Button to={`/preview-environments`}>
-          <i className="material-icons">keyboard_backspace</i>
-          Back
-        </Button>
-        <Title>
-          <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>
-        </Title>
-      </HeaderSection>
-
-      <Heading>Select a Repository</Heading>
-      <br />
-      {/* <RepoList
-        actionConfig={actionConfig}
-        setActionConfig={(a: GithubActionConfigType) => {
-          setActionConfig(a);
-          setRepo(a.git_repo);
-        }}
-        readOnly={false}
-        filteredRepos={filteredRepos}
-      /> */}
-      <ConnectNewRepoActionConfEditor
-        actionConfig={actionConfig}
-        setActionConfig={(actionConfig: ActionConfigType) => {
-          setActionConfig((currentActionConfig: ActionConfigType) => ({
-            ...currentActionConfig,
-            ...actionConfig,
-          }));
-        }}
-      />
-      <HelperContainer>
-        Note: you will need to add a <CodeBlock>porter.yaml</CodeBlock> file to
-        create a preview environment.
-        <DocsHelper
-          disableMargin
-          tooltipText="A Porter YAML file is a declarative set of resources that Porter uses to build and update your preview environment deployments."
-          link="https://docs.porter.run/preview-environments/porter-yaml-reference"
-        />
-      </HelperContainer>
-
-      {/* <StyledAdvancedBuildSettings
-        showSettings={showSettings}
-        isCurrent={true}
-        onClick={() => {
-          setShowSettings(!showSettings);
-        }}
-      > */}
-      <StyledAdvancedBuildSettings
-        showSettings={showSettings}
-        isCurrent={true}
-        onClick={() => {
-          setShowSettings(!showSettings);
-        }}
-      >
-        <AdvancedBuildTitle>
-          <i className="material-icons dropdown">arrow_drop_down</i>
-          Configure Additonal settings
-        </AdvancedBuildTitle>
-      </StyledAdvancedBuildSettings>
-      <AnimateHeight height={showSettings ? "auto" : 0} duration={1000}>
-        <StyledSourceBox>
-          <Text size={16}>Deploy from branches</Text>
-          <Helper style={{ marginTop: "10px", marginBottom: "10px" }}>
-            {" "}
-            Choose the list of branches that you want to deploy changes from.
-          </Helper>
-          <BranchFilterSelector
-            onChange={setDeployBranches}
-            options={availableBranches}
-            value={deployBranches}
-            showLoading={isLoadingBranches}
-          />
-
-          <Text size={16}>Select allowed branches</Text>
-          <Helper style={{ marginTop: "10px", marginBottom: "10px" }}>
-            {" "}
-            If the pull request has a base branch included in this list, it will
-            be allowed to be deployed.
-            <br />
-            (Leave empty to allow all branches)
-          </Helper>
-          <BranchFilterSelector
-            onChange={setBaseBranches}
-            options={availableBranches}
-            value={baseBranches}
-            showLoading={isLoadingBranches}
-          />
-          <Text size={16}>Automatic pull request deployments</Text>
-          <Helper style={{ marginTop: "10px", marginBottom: "10px" }}>
-            If you enable this option, the new pull requests will be
-            automatically deployed.
-          </Helper>
-          <CheckboxWrapper>
-            <CheckboxRow
-              label="Enable automatic deploys"
-              checked={enableAutomaticDeployments}
-              toggle={() =>
-                setEnableAutomaticDeployments(!enableAutomaticDeployments)
-              }
-              wrapperStyles={{
-                disableMargin: true,
-              }}
-            />
-          </CheckboxWrapper>
-          <Spacer y={2} />
-          <Text size={16}>Disable new comments for new deployments</Text>
-          <Helper style={{ marginTop: "10px", marginBottom: "10px" }}>
-            When enabled new comments will not be created for new deployments.
-            Instead the last comment will be updated.
-          </Helper>
-          <CheckboxWrapper>
-            <CheckboxRow
-              label="Disable new comments for deployments"
-              checked={isNewCommentsDisabled}
-              toggle={() => setIsNewCommentsDisabled(!isNewCommentsDisabled)}
-              wrapperStyles={{
-                disableMargin: true,
-              }}
-            />
-          </CheckboxWrapper>
-          <Spacer y={2} />
-
-          <Text size={16}>Namespace labels</Text>
-          <Helper style={{ marginTop: "10px", marginBottom: "10px" }}>
-            Custom labels to be injected into the Kubernetes namespace created
-            for each deployment.
-          </Helper>
-          <NamespaceLabels
-            values={namespaceLabels}
-            setValues={(x: KeyValueType[]) => {
-              let labels: KeyValueType[] = [];
-              x.forEach((entry) => {
-                labels.push({ key: entry.key, value: entry.value });
-              });
-              setNamespaceLabels(labels);
-            }}
-          />
-        </StyledSourceBox>
-      </AnimateHeight>
-
-      <ActionContainer>
-        <SaveButton
-          text="Add repository"
-          disabled={actionConfig.git_repo_id ? false : true}
-          onClick={addRepo}
-          makeFlush={true}
-          clearPosition={true}
-          status={status}
-          statusPosition={"left"}
-        ></SaveButton>
-      </ActionContainer>
-    </>
-  );
-};
-
-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;
-`;

+ 0 - 116
dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepoActionConfEditor.tsx

@@ -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<Props> = ({
-  actionConfig,
-  setBranch,
-  setActionConfig,
-  setFolderPath,
-  setDockerfilePath,
-  setBuildView,
-  setPorterYamlPath,
-}) => {
-  if (!actionConfig.git_repo) {
-    return (
-      <ExpandedWrapperAlt>
-        <RepoList
-          actionConfig={actionConfig}
-          setActionConfig={(x: ActionConfigType) => setActionConfig(x)}
-          readOnly={false}
-        />
-      </ExpandedWrapperAlt>
-    );
-  } else {
-    return (
-      <>
-        <Input
-          disabled={true}
-          label="GitHub repository:"
-          width="100%"
-          value={actionConfig?.git_repo}
-          setValue={() => {}}
-          placeholder=""
-        />
-        <BackButton
-          width="135px"
-          onClick={() => {
-            setActionConfig({ ...defaultActionConfig });
-            setBranch ? setBranch("") : null;
-            setFolderPath ? setFolderPath("") : null;
-            setDockerfilePath ? setDockerfilePath("") : null;
-            setBuildView ? setBuildView("buildpacks") : null;
-            setPorterYamlPath && setPorterYamlPath("");
-          }}
-        >
-          <i className="material-icons">keyboard_backspace</i>
-          Select repo
-        </BackButton>
-      </>
-    );
-  }
-};
-
-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;
-  }
-`;

+ 0 - 58
dashboard/src/main/home/cluster-dashboard/preview-environments/components/ActionButton.tsx

@@ -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;
-  }
-`;

+ 0 - 97
dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx

@@ -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 (
-    <>
-      <SearchSelector
-        options={filteredBranches}
-        onSelect={(newBranch) => handleAddBranch(newBranch)}
-        getOptionLabel={(option) => option}
-        placeholder={placeholder}
-        showLoading={showLoading}
-      />
-      {/* List selected branches  */}
-
-      <BranchRowList>
-        {value.map((branch) => (
-          <BranchRow key={branch}>
-            <div>{branch}</div>
-            <RemoveBranchButton onClick={() => handleDeleteBranch(branch)}>
-              x
-            </RemoveBranchButton>
-          </BranchRow>
-        ))}
-      </BranchRowList>
-    </>
-  );
-};
-
-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;
-`;

+ 0 - 173
dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx

@@ -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("<token>", {}, {});
-      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 (
-      <Container>
-        <Button disabled={true} to="">
-          <img src={pr_icon} alt="Pull request icon" />
-          Loading . . .
-        </Button>
-      </Container>
-    );
-  }
-
-  if (!hasGHAccountConnected) {
-    return (
-      <>
-        <Container>
-          <Button {...getButtonProps()}>
-            <img src={pr_icon} alt="Pull request icon" />
-            Connect GitHub account
-          </Button>
-        </Container>
-      </>
-    );
-  }
-
-  return (
-    <>
-      <Container>
-        <Button {...getButtonProps()}>
-          <i className="material-icons">add</i> Add repository
-        </Button>
-      </Container>
-    </>
-  );
-};
-
-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`
-`;

+ 0 - 164
dashboard/src/main/home/cluster-dashboard/preview-environments/components/NamespaceLabels.tsx

@@ -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 (
-    <>
-      <StyledInputArray>
-        {!!values?.length &&
-          values.map((entry: KeyValueType, i: number) => {
-            return (
-              <InputWrapper key={i}>
-                <Input
-                  placeholder="ex: key"
-                  width="270px"
-                  value={entry.key}
-                  onChange={(e: any) => {
-                    let _values = values;
-                    _values[i].key = e.target.value;
-                    setValues(_values);
-                  }}
-                />
-                <Spacer />
-                <Input
-                  placeholder="ex: value"
-                  width="270px"
-                  value={entry.value}
-                  onChange={(e: any) => {
-                    let _values = values;
-                    _values[i].value = e.target.value;
-                    setValues(_values);
-                  }}
-                />
-                <DeleteButton
-                  onClick={() => {
-                    let _values = values;
-                    _values = _values.filter((val) => val.key !== entry.key);
-                    setValues(_values);
-                  }}
-                >
-                  <i className="material-icons">cancel</i>
-                </DeleteButton>
-              </InputWrapper>
-            );
-          })}
-        <InputWrapper>
-          <AddRowButton
-            onClick={() => {
-              let _values = values;
-              _values.push({
-                key: "",
-                value: "",
-              });
-              setValues(_values);
-            }}
-          >
-            <i className="material-icons">add</i> Add Row
-          </AddRowButton>
-          <Spacer />
-        </InputWrapper>
-      </StyledInputArray>
-    </>
-  );
-};
-
-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;
-`;

+ 0 - 98
dashboard/src/main/home/cluster-dashboard/preview-environments/components/PorterYAMLErrorsModal.tsx

@@ -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 (
-    <Modal onRequestClose={() => onClose()} height="auto">
-      <TitleSection icon={danger}>
-        <Text>porter.yaml</Text>
-      </TitleSection>
-      <InfoRow>
-        <InfoTab>
-          <img src={info} /> <Bold>Repo:</Bold>
-          {repo}
-        </InfoTab>
-        {branch ? (
-          <InfoTab>
-            <img src={info} /> <Bold>Branch:</Bold>
-            {branch}
-          </InfoTab>
-        ) : null}
-      </InfoRow>
-      <Message>
-        {errors.map((el) => {
-          return (
-            <div>
-              {"- "}
-              {el}
-            </div>
-          );
-        })}
-      </Message>
-    </Modal>
-  );
-};
-
-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;

+ 0 - 104
dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx

@@ -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<string>(
-    "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("<token>", {}, {})
-      .then(({ data }) => {
-        setGithubStatus(data);
-      })
-      .catch((err) => {
-        console.error(err);
-      });
-  }, []);
-
-  return (
-    <>
-      <DashboardHeader
-        image={PullRequestIcon}
-        title="Preview environments"
-        description="Create full-stack preview environments for your pull requests."
-        disableLineBreak
-        capitalize={false}
-      />
-      {githubStatus != "no active incidents" ? (
-        <>
-          <Banner type="error">
-            GitHub has an ongoing incident.
-            <StyledLink href={`${githubStatus}`} target="_blank">
-              View details
-            </StyledLink>
-          </Banner>
-          <Spacer y={1} />
-        </>
-      ) : 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;
-`;

+ 0 - 84
dashboard/src/main/home/cluster-dashboard/preview-environments/components/styled.tsx

@@ -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<HTMLDivElement>
-> = ({ children, tooltipText, className, ...divProps }) => {
-  const [showTooltip, setShowTooltip] = useState(false);
-  return (
-    <StyledTooltipWrapper
-      {...divProps}
-      className={className}
-      onMouseOver={() => setShowTooltip(true)}
-      onMouseOut={() => setShowTooltip(false)}
-    >
-      <StyledEllipsisTextWrapper>{children}</StyledEllipsisTextWrapper>
-      {tooltipText && showTooltip ? <Tooltip>{tooltipText}</Tooltip> : null}
-    </StyledTooltipWrapper>
-  );
-};
-
-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;
-  }
-`;

+ 0 - 674
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx

@@ -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<HTMLDivElement>();
-  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 (
-    <div
-      style={{
-        position: "relative",
-      }}
-    >
-      <I
-        className="material-icons"
-        onClick={(e) => {
-          e.preventDefault();
-          e.stopPropagation();
-          setExpanded((expanded) => !expanded);
-        }}
-      >
-        more_vert
-      </I>
-      <ActionsDropdownWrapper expanded={expanded}>
-        <ActionsDropdown ref={wrapperRef}>
-          {options.length ? (
-            <ActionsScrollableWrapper>
-              {options
-                .filter((option) => option.active)
-                .map(({ label, action }, idx) => {
-                  return (
-                    <ActionsRow
-                      isLast={idx === options.length - 1}
-                      onClick={action}
-                      key={label}
-                    >
-                      <ActionsRowText>{label}</ActionsRowText>
-                    </ActionsRow>
-                  );
-                })}
-            </ActionsScrollableWrapper>
-          ) : null}
-        </ActionsDropdown>
-      </ActionsDropdownWrapper>
-    </div>
-  );
-};
-
-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(
-        "<token>",
-        {},
-        {
-          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(
-        "<token>",
-        {},
-        {
-          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(
-        "<token>",
-        {},
-        {
-          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 (
-    <DeploymentCardWrapper
-      to={`/preview-environments/details/${deployment.id}?environment_id=${deployment.environment_id}`}
-    >
-      <DataContainer>
-        <PRName>
-          <PRIcon src={pr_icon} alt="pull request icon" />
-          <EllipsisTextWrapper tooltipText={deployment.gh_pr_name}>
-            <StyledLink
-              onClick={(e) => {
-                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}
-            </StyledLink>
-          </EllipsisTextWrapper>
-          {deployment.gh_pr_branch_from &&
-          deployment.gh_pr_branch_into &&
-          deployment.gh_pr_branch_from !== deployment.gh_pr_branch_into ? (
-            <MergeInfoWrapper>
-              <MergeInfo
-                onMouseOver={() => setShowMergeInfoTooltip(true)}
-                onMouseOut={() => setShowMergeInfoTooltip(false)}
-              >
-                {deployment.gh_pr_branch_from}
-                <i className="material-icons">arrow_forward</i>
-                {deployment.gh_pr_branch_into}
-              </MergeInfo>
-              {showMergeInfoTooltip && (
-                <Tooltip>
-                  {deployment.gh_pr_branch_from} {"->"}{" "}
-                  {deployment.gh_pr_branch_into}
-                </Tooltip>
-              )}
-            </MergeInfoWrapper>
-          ) : null}
-        </PRName>
-
-        <Flex>
-          <StatusContainer>
-            <Status>
-              <StatusDot status={deployment.status} />
-              {capitalize(deployment.status)}
-            </Status>
-          </StatusContainer>
-          <DeploymentImageContainer>
-            <InfoWrapper>
-              <SepDot>•</SepDot>
-              <LastDeployed>
-                Last updated {readableDate(deployment.updated_at)}
-              </LastDeployed>
-            </InfoWrapper>
-          </DeploymentImageContainer>
-        </Flex>
-      </DataContainer>
-      <Flex>
-        {!isDeleting ? (
-          <>
-            {deployment.status === DeploymentStatus.Failed ||
-            deployment.status === DeploymentStatus.TimedOut ? (
-              <>
-                <MaterialTooltip title="Re run last github workflow">
-                  <ReRunButton
-                    onClick={() => reRunWorkflow()}
-                    disabled={isReRunningWorkflow}
-                    hasError={hasErrorOnReRun}
-                  >
-                    <i className="material-icons-outlined">loop</i>
-                  </ReRunButton>
-                </MaterialTooltip>
-              </>
-            ) : null}
-
-            {deployment.subdomain &&
-            deployment.status === DeploymentStatus.Created ? (
-              <RowButton
-                onClick={(e) => {
-                  e.preventDefault();
-                  e.stopPropagation();
-
-                  window.open(deployment.subdomain, "_blank");
-                }}
-                key={deployment.subdomain}
-              >
-                <i className="material-icons">open_in_new</i>
-                View Live
-              </RowButton>
-            ) : null}
-            <DeploymentCardActionsDropdown options={DeploymentCardActions} />
-            {/* <Button
-              onClick={() => {
-                setCurrentOverlay({
-                  message: `Are you sure you want to delete this deployment?`,
-                  onYes: deleteDeployment,
-                  onNo: () => setCurrentOverlay(null),
-                });
-              }}
-            >
-              <i className="material-icons">delete</i>
-              Delete
-            </Button> */}
-          </>
-        ) : (
-          <DeleteMessage>
-            Deleting
-            <Dot delay="0s" />
-            <Dot delay="0.1s" />
-            <Dot delay="0.2s" />
-          </DeleteMessage>
-        )}
-      </Flex>
-    </DeploymentCardWrapper>
-  );
-};
-
-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;
-  }
-`;

+ 0 - 767
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx

@@ -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<PRDeployment>(null);
-  const [environmentId, setEnvironmentId] = useState("");
-  const [showRepoTooltip, setShowRepoTooltip] = useState(false);
-  const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
-  const [expandedPorterYAMLErrors, setExpandedPorterYAMLErrors] = useState<
-    string[]
-  >([]);
-  const [
-    expandedLastDeploymentErrors,
-    setExpandedLastDeploymentErrors,
-  ] = useState<string[]>([]);
-
-  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(
-        "<token>",
-        {
-          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 <PreviewEnvDeleted />;
-  }
-  const repository = `${prDeployment.gh_repo_owner}/${prDeployment.gh_repo_name}`;
-
-  const deleteDeployment = () => {
-    //setIsDeleting(true);
-
-    api
-      .deletePRDeployment(
-        "<token>",
-        {},
-        {
-          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 (
-      <>
-        <BreadcrumbRow>
-          <Breadcrumb to={`/preview-environments/deployments/settings`}>
-            <ArrowIcon src={PullRequestIcon} />
-            <Wrap>Preview environments</Wrap>
-          </Breadcrumb>
-          <Slash>/</Slash>
-          <Breadcrumb
-            to={`/preview-environments/deployments/${environmentId}/${repository}`}
-          >
-            <GitIcon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
-            <Wrap>{repository}</Wrap>
-          </Breadcrumb>
-        </BreadcrumbRow>
-        <StyledExpandedChart>
-          <HeaderWrapper>
-            <Flex>
-              <Title
-                icon={pr_icon}
-                iconWidth="25px"
-                onClick={() =>
-                  window.open(
-                    `https://github.com/${repository}/pull/${prDeployment.pull_request_id}`,
-                    "_blank"
-                  )
-                }
-              >
-                {prDeployment.gh_pr_name}
-              </Title>
-              <span
-                onClick={() => setShowDropdown(!showDropdown)}
-                style={{ cursor: "pointer" }}
-              >
-                <I className="material-icons">settings</I>
-                {showDropdown && (
-                  <DeleteDropdown>
-                    <Button onClick={deleteDeployment}>Delete</Button>
-                  </DeleteDropdown>
-                )}
-              </span>
-            </Flex>
-            <InfoWrapper>
-              {prDeployment.subdomain && (
-                <PRLink to={prDeployment.subdomain} target="_blank">
-                  <i className="material-icons">link</i>
-                  {prDeployment.subdomain}
-                </PRLink>
-              )}
-            </InfoWrapper>
-
-            <Flex>
-              <Status>
-                <StatusDot status={prDeployment.status} />
-                {capitalize(prDeployment.status)}
-              </Status>
-              <Dot>•</Dot>
-              <DeploymentImageContainer>
-                <DeploymentTypeIcon src={integrationList.repo.icon} />
-                <RepositoryName
-                  onMouseOver={() => {
-                    setShowRepoTooltip(true);
-                  }}
-                  onMouseOut={() => {
-                    setShowRepoTooltip(false);
-                  }}
-                >
-                  {repository}
-                </RepositoryName>
-                {showRepoTooltip && <Tooltip>{repository}</Tooltip>}
-              </DeploymentImageContainer>
-              <Dot>•</Dot>
-              <GHALink
-                to={`https://github.com/${prDeployment.gh_repo_owner}/${prDeployment.gh_repo_name}/pull/${prDeployment.pull_request_id}`}
-                target="_blank"
-              >
-                <GithubIcon />
-                View PR
-                <i className="material-icons">open_in_new</i>
-              </GHALink>
-            </Flex>
-            <LinkToActionsWrapper></LinkToActionsWrapper>
-          </HeaderWrapper>
-          <ChartListWrapper>
-            <Placeholder height="370px">
-              This preview deployment has not been created yet.{" "}
-              <ViewLastWorkflowLink
-                to={`https://github.com/${prDeployment.gh_repo_owner}/${prDeployment.gh_repo_name}/actions`}
-                target="_blank"
-              >
-                View last workflow
-              </ViewLastWorkflowLink>
-            </Placeholder>
-          </ChartListWrapper>
-        </StyledExpandedChart>
-      </>
-    );
-  }
-
-  return (
-    <>
-      {expandedPorterYAMLErrors.length > 0 && (
-        <Modal
-          onRequestClose={() => setExpandedPorterYAMLErrors([])}
-          height="auto"
-        >
-          <Message>
-            {expandedPorterYAMLErrors.map((el) => {
-              return (
-                <div>
-                  {"- "}
-                  {el}
-                </div>
-              );
-            })}
-          </Message>
-        </Modal>
-      )}
-      {expandedLastDeploymentErrors.length > 0 && (
-        <Modal
-          onRequestClose={() => setExpandedLastDeploymentErrors([])}
-          height="auto"
-        >
-          <Message>
-            {expandedLastDeploymentErrors.map((el) => {
-              return (
-                <div>
-                  {"- "}
-                  {el}
-                </div>
-              );
-            })}
-          </Message>
-        </Modal>
-      )}
-      <BreadcrumbRow>
-        <Breadcrumb to={`/preview-environments/deployments/settings`}>
-          <ArrowIcon src={PullRequestIcon} />
-          <Wrap>Preview environments</Wrap>
-        </Breadcrumb>
-        <Slash>/</Slash>
-        <Breadcrumb
-          to={`/preview-environments/deployments/${environmentId}/${repository}`}
-        >
-          <GitIcon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
-          <Wrap>{repository}</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <StyledExpandedChart>
-        <HeaderWrapper>
-          <Title
-            icon={pr_icon}
-            iconWidth="25px"
-            onClick={() =>
-              window.open(
-                `https://github.com/${repository}/pull/${prDeployment.pull_request_id}`,
-                "_blank"
-              )
-            }
-          >
-            {prDeployment.gh_pr_name}
-          </Title>
-          <InfoWrapper>
-            {prDeployment.subdomain && (
-              <PRLink to={prDeployment.subdomain} target="_blank">
-                <i className="material-icons">link</i>
-                {prDeployment.subdomain}
-              </PRLink>
-            )}
-            <TagWrapper>
-              Namespace <NamespaceTag>{prDeployment.namespace}</NamespaceTag>
-            </TagWrapper>
-          </InfoWrapper>
-          <Flex>
-            <Status>
-              <StatusDot status={prDeployment.status} />
-              {capitalize(prDeployment.status)}
-            </Status>
-            <Dot>•</Dot>
-            <DeploymentImageContainer>
-              <DeploymentTypeIcon src={integrationList.repo.icon} />
-              <RepositoryName
-                onMouseOver={() => {
-                  setShowRepoTooltip(true);
-                }}
-                onMouseOut={() => {
-                  setShowRepoTooltip(false);
-                }}
-              >
-                {repository}
-              </RepositoryName>
-              {showRepoTooltip && <Tooltip>{repository}</Tooltip>}
-            </DeploymentImageContainer>
-            <Dot>•</Dot>
-            {prDeployment.last_workflow_run_url ? (
-              <GHALink to={prDeployment.last_workflow_run_url} target="_blank">
-                <img src={github} /> View last workflow run
-                <i className="material-icons">open_in_new</i>
-              </GHALink>
-            ) : null}
-          </Flex>
-          <LinkToActionsWrapper></LinkToActionsWrapper>
-        </HeaderWrapper>
-        {porterYAMLErrors.length > 0 ? (
-          <ErrorBannerWrapper>
-            <Banner type="error">
-              Your porter.yaml file has errors. Please fix them before
-              deploying.
-              <LinkButton
-                onClick={() => {
-                  setExpandedPorterYAMLErrors(porterYAMLErrors);
-                }}
-              >
-                View details
-              </LinkButton>
-            </Banner>
-          </ErrorBannerWrapper>
-        ) : null}
-        {prDeployment.last_errors.length > 0 ? (
-          <ErrorBannerWrapper style={{ marginTop: -20 }}>
-            <Banner type="error">
-              The previous deployment had errors.
-              <LinkButton
-                onClick={() => {
-                  setExpandedLastDeploymentErrors(
-                    prDeployment.last_errors.split(",")
-                  );
-                }}
-              >
-                View details
-              </LinkButton>
-            </Banner>
-          </ErrorBannerWrapper>
-        ) : null}
-        <ChartListWrapper>
-          <ChartList
-            currentCluster={context.currentCluster}
-            currentView="cluster-dashboard"
-            sortType="Newest"
-            namespace={prDeployment.namespace}
-            disableBottomPadding
-            closeChartRedirectUrl={`${window.location.pathname}${window.location.search}`}
-          />
-          <ChartList
-            currentView="jobs"
-            noPlaceholder={true}
-            lastRunStatus="all"
-            currentCluster={currentCluster}
-            namespace={prDeployment.namespace}
-            sortType="Newest"
-          />
-        </ChartListWrapper>
-      </StyledExpandedChart>
-    </>
-  );
-};
-
-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;
-  }
-`;

+ 0 - 602
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentList.tsx

@@ -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<PRDeployment[]>([]);
-  const [pullRequests, setPullRequests] = useState<PullRequest[]>([]);
-  const [searchValue, setSearchValue] = useState("");
-  const [newCommentsDisabled, setNewCommentsDisabled] = useState(false);
-  const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
-  const [expandedPorterYAMLErrors, setExpandedPorterYAMLErrors] = useState<
-    string[]
-  >([]);
-
-  const [statusSelectorVal, setStatusSelectorVal] =
-    useState<AvailableStatusFiltersType>("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(
-      "<token>",
-      {},
-      {
-        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<PRDeployment>(
-      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 (
-        <LoadingWrapper>
-          <Loading />
-        </LoadingWrapper>
-      );
-    }
-
-    if (!deploymentList.length) {
-      return (
-        <Placeholder height="calc(100vh - 400px)">
-          No preview developments have been found. Open a PR to create a new
-          preview environment.
-        </Placeholder>
-      );
-    }
-
-    if (!filteredDeployments.length) {
-      return (
-        <Placeholder height="calc(100vh - 400px)">
-          No preview developments have been found with the given filter.
-        </Placeholder>
-      );
-    }
-
-    return (
-      <>
-        {filteredDeployments.map((d: any) => {
-          return (
-            <DeploymentCard
-              key={d.id}
-              deployment={d}
-              onDelete={handleRefresh}
-              onReEnable={handleRefresh}
-              onReRun={handleRefresh}
-            />
-          );
-        })}
-      </>
-    );
-  };
-
-  useEffect(() => {
-    pushQueryParams({ status_filter: statusSelectorVal });
-  }, [statusSelectorVal]);
-
-  return (
-    <>
-      <PorterYAMLErrorsModal
-        errors={expandedPorterYAMLErrors}
-        onClose={() => {
-          setExpandedPorterYAMLErrors([]);
-        }}
-        repo={selectedRepo}
-      />
-
-      <BreadcrumbRow>
-        <Breadcrumb to="/preview-environments">
-          <ArrowIcon src={pullRequestIcon} />
-          <Wrap>Preview environments</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <DashboardHeader
-        image="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png"
-        title={
-          <Flex>
-            <StyledLink
-              to={`https://github.com/${selectedRepo}`}
-              target="_blank"
-            >
-              {selectedRepo}
-            </StyledLink>
-            <DynamicLink
-              to={`/preview-environments/deployments/${environment_id}/${repo_owner}/${repo_name}/settings`}
-            >
-              <I className="material-icons">settings</I>
-            </DynamicLink>
-          </Flex>
-        }
-        description={`Preview environments for the ${selectedRepo} repository.`}
-        disableLineBreak
-        capitalize={false}
-      />
-      {/* {porterYAMLErrors.length > 0 ? (
-        <PorterYAMLBannerWrapper>
-          <Banner type="warning">
-            No porter.yaml file in the default branch.
-            <LinkButton
-              onClick={() => {
-                setExpandedPorterYAMLErrors(porterYAMLErrors);
-              }}
-            >
-              Learn more
-            </LinkButton>
-          </Banner>
-        </PorterYAMLBannerWrapper>
-      ) : null} */}
-      <FlexRow>
-        <Flex>
-          <SearchRowWrapper>
-            <SearchBarWrapper>
-              <i className="material-icons">search</i>
-              <SearchInput
-                value={searchValue}
-                onChange={(e: any) => {
-                  setSearchValue(e.target.value);
-                }}
-                placeholder="Search"
-              />
-            </SearchBarWrapper>
-          </SearchRowWrapper>
-          <RadioFilter
-            icon={filterOutline}
-            selected={statusSelectorVal}
-            setSelected={setStatusSelectorVal}
-            options={AvailableStatusFilters.map((filter) => ({
-              value: filter,
-              label: _.startCase(filter),
-            }))}
-            name="Status"
-          />
-        </Flex>
-        <Flex>
-          <RefreshButton color={"#7d7d81"} onClick={handleRefresh}>
-            <i className="material-icons">refresh</i>
-          </RefreshButton>
-          <RadioFilter
-            icon={sort}
-            selected={sortOrder}
-            setSelected={setSortOrder}
-            options={[
-              { label: "Newest", value: "Newest" },
-              { label: "Oldest", value: "Oldest" },
-              { label: "Alphabetical", value: "Alphabetical" },
-            ]}
-            name="Sort"
-          />
-          <CreatePreviewEnvironmentButton
-            to={`/preview-environments/deployments/${environment_id}/${repo_owner}/${repo_name}/create`}
-          >
-            <i className="material-icons">add</i> New preview deployment
-          </CreatePreviewEnvironmentButton>
-        </Flex>
-      </FlexRow>
-      <Container>
-        <EventsGrid>{renderDeploymentList()}</EventsGrid>
-      </Container>
-    </>
-  );
-};
-
-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;
-`;

+ 0 - 76
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PreviewEnvDeleted.tsx

@@ -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<PreviewEnvDeletedProps> = ({}) => {
-  const history = useHistory();
-
-  const handleBackButtonClick = () => {
-    history.push("/preview-environments/deployments/");
-  };
-
-  return (
-    <DeletedContainer>
-      <Text size={16}>This preview environment has been deleted.</Text>
-      <Spacer y={0.5} />
-      <Button width="75px" onClick={handleBackButtonClick}>
-        <i className="material-icons">keyboard_backspace</i>
-        Back
-      </Button>
-    </DeletedContainer>
-  );
-};
-
-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;

+ 0 - 538
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx

@@ -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<boolean>(false);
-  const [showErrorsModal, setShowErrorsModal] = useState<boolean>(false);
-  const { currentProject, currentCluster, setCurrentError } = useContext(
-    Context
-  );
-
-  const {
-    data: environment,
-    isLoading: environmentLoading,
-  } = useQuery<Environment>(
-    ["environment", currentProject.id, currentCluster.id, environmentID],
-    async () => {
-      const { data: environment } = await api.getEnvironment<Environment>(
-        "<token>",
-        {},
-        {
-          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<string[]>(
-    ["branches", currentProject.id, currentCluster.id, environment],
-    async () => {
-      try {
-        const res = await api.getBranches<string[]>(
-          "<token>",
-          {},
-          {
-            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<boolean>(
-    false
-  );
-  const environmentGitDeployBranches = environment?.git_deploy_branches ?? [];
-  const [selectedBranch, setSelectedBranch] = useState<string>(null);
-  const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
-
-  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<string>(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 (
-      <>
-        <Br height="30px" />
-        <Placeholder minHeight="50vh">
-          <Loading />
-        </Placeholder>
-      </>
-    );
-  }
-
-  if (!branches?.length) {
-    return (
-      <>
-        <Br height="30px" />
-        <Placeholder height="370px">You do not have any branches.</Placeholder>
-      </>
-    );
-  }
-
-  return (
-    <>
-      <Helper>
-        Select a branch to preview. Branches must contain a{" "}
-        <Code>porter.yaml</Code> file.
-      </Helper>
-      <FlexRow>
-        <Flex>
-          <SearchRowWrapper>
-            <SearchBarWrapper>
-              <i className="material-icons">search</i>
-              <SearchInput
-                value={searchValue}
-                onChange={(e: any) => {
-                  setSelectedBranch(undefined);
-                  setPorterYAMLErrors([]);
-                  setSearchValue(e.target.value);
-                }}
-                placeholder="Search"
-              />
-            </SearchBarWrapper>
-          </SearchRowWrapper>
-        </Flex>
-        <Flex>
-          <RefreshButton color={"#7d7d81"} onClick={handleRefresh}>
-            <i className="material-icons">refresh</i>
-          </RefreshButton>
-          <RadioFilter
-            icon={sort}
-            selected={sortOrder}
-            setSelected={setSortOrder}
-            options={[{ label: "Alphabetical", value: "Alphabetical" }]}
-            name="Sort"
-          />
-        </Flex>
-      </FlexRow>
-      <Br height="10px" />
-      <BranchList>
-        {(filteredBranches ?? []).map((branch, i) => (
-          <BranchRow
-            onClick={() => handleRowItemClick(branch)}
-            isLast={i === filteredBranches.length - 1}
-            isSelected={branch === selectedBranch}
-          >
-            <BranchName>
-              <BranchIcon src={pr_icon} alt="branch icon" />
-              <EllipsisTextWrapper tooltipText={branch}>
-                {branch}
-              </EllipsisTextWrapper>
-            </BranchName>
-          </BranchRow>
-        ))}
-      </BranchList>
-      {showErrorsModal && selectedBranch ? (
-        <PorterYAMLErrorsModal
-          errors={porterYAMLErrors}
-          onClose={() => setShowErrorsModal(false)}
-          repo={environment.git_repo_name + "/" + environment.git_repo_owner}
-          branch={selectedBranch}
-        />
-      ) : null}
-      {selectedBranch && porterYAMLErrors.length ? (
-        <ValidationErrorBannerWrapper>
-          <Banner type="warning">
-            We found some errors in the porter.yaml file in the&nbsp;
-            {selectedBranch}&nbsp;branch. &nbsp;
-            <LearnMoreButton onClick={() => setShowErrorsModal(true)}>
-              Learn more
-            </LearnMoreButton>
-          </Banner>
-        </ValidationErrorBannerWrapper>
-      ) : null}
-      <CreatePreviewDeploymentWrapper>
-        {showCreatePreviewModal &&
-          selectedBranch &&
-          porterYAMLErrors.length == 0 && (
-            <Modal
-              title="Create Preview Environment"
-              closeModal={() => setShowCreatePreviewModal(false)}
-            >
-              <>
-                <Text color="helper">
-                  Create Preview Deployment for branch: {selectedBranch}?
-                </Text>
-                <Spacer y={1} />
-                <SubmitButton
-                  onClick={() => updateDeployBranchesMutation.mutate()}
-                  disabled={
-                    updateDeployBranchesMutation.isLoading ||
-                    loading ||
-                    porterYAMLErrors.length > 0 ||
-                    !selectedBranch
-                  }
-                >
-                  {updateDeployBranchesMutation.isLoading
-                    ? "Creating..."
-                    : "Create Preview Deployment"}
-                </SubmitButton>
-              </>
-            </Modal>
-          )}
-        {selectedBranch && porterYAMLErrors.length ? (
-          <RevalidatePorterYAMLSpanWrapper>
-            Please fix your porter.yaml file to continue.{" "}
-            <RevalidateSpan
-              onClick={(e) => {
-                e.preventDefault();
-                e.stopPropagation();
-
-                if (!selectedBranch) {
-                  return;
-                }
-
-                handleRowItemClick(selectedBranch);
-              }}
-            >
-              Refresh
-            </RevalidateSpan>
-          </RevalidatePorterYAMLSpanWrapper>
-        ) : null}
-      </CreatePreviewDeploymentWrapper>
-    </>
-  );
-};
-
-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;
-  }
-`;

+ 0 - 115
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateEnvironment.tsx

@@ -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<typeof TAB_OPTIONS[0]>(
-    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 (
-    <>
-      <BreadcrumbRow>
-        <Breadcrumb to={`/preview-environments/deployments/settings`}>
-          <ArrowIcon src={PullRequestIcon} />
-          <Wrap>Preview environments</Wrap>
-        </Breadcrumb>
-        <Slash>/</Slash>
-        <Breadcrumb
-          to={`/preview-environments/deployments/${environment_id}/${selectedRepo}`}
-        >
-          <Icon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
-          <Wrap>{selectedRepo}</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <DashboardHeader
-        title="Create a preview deployment"
-        disableLineBreak
-        capitalize={false}
-      />
-      <DarkMatter />
-      <TabSelector
-        options={TAB_OPTIONS}
-        currentTab={currentTab.value}
-        setCurrentTab={(value: string) =>
-          setCurrentTab(TAB_OPTIONS.find((tab) => tab.value === value))
-        }
-      />
-
-      {currentTab.value === "pull_requests" ? (
-        <CreatePREnvironment environmentID={environment_id} />
-      ) : (
-        <CreateBranchEnvironment environmentID={environment_id} />
-      )}
-    </>
-  );
-};
-
-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;
-  }
-`;

+ 0 - 591
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreatePREnvironment.tsx

@@ -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<boolean>(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<PullRequest>();
-  const [loading, setLoading] = useState(false);
-  const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
-  const [showCreatePreviewModal, setShowCreatePreviewModal] = useState<boolean>(
-    false
-  );
-
-  const handleCreatePreviewDeployment = async () => {
-    try {
-      await api.createPreviewEnvironmentDeployment("<token>", 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<PullRequest>(
-      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 (
-      <>
-        <Br height="30px" />
-        <Placeholder minHeight="50vh">
-          <Loading />
-        </Placeholder>
-      </>
-    );
-  }
-
-  if (!pullRequests.length) {
-    return (
-      <>
-        <Br height="30px" />
-        <Placeholder height="50vh">{`You do not have any pull requests.`}</Placeholder>
-      </>
-    );
-  }
-
-  return (
-    <>
-      <Helper>
-        Select an open pull request to preview. Pull requests must contain a{" "}
-        <Code>porter.yaml</Code> file.
-      </Helper>
-      <FlexRow>
-        <Flex>
-          <SearchRowWrapper>
-            <SearchBarWrapper>
-              <i className="material-icons">search</i>
-              <SearchInput
-                value={searchValue}
-                onChange={(e: any) => {
-                  setSelectedPR(undefined);
-                  setPorterYAMLErrors([]);
-                  setSearchValue(e.target.value);
-                }}
-                placeholder="Search"
-              />
-            </SearchBarWrapper>
-          </SearchRowWrapper>
-        </Flex>
-        <Flex>
-          <RefreshButton color={"#7d7d81"} onClick={handleRefresh}>
-            <i className="material-icons">refresh</i>
-          </RefreshButton>
-          <RadioFilter
-            icon={sort}
-            selected={sortOrder}
-            setSelected={setSortOrder}
-            options={[
-              { label: "Recently Updated", value: "Recently Updated" },
-              { label: "Newest", value: "Newest" },
-              { label: "Oldest", value: "Oldest" },
-              { label: "Alphabetical", value: "Alphabetical" },
-            ]}
-            name="Sort"
-          />
-        </Flex>
-      </FlexRow>
-      <Br height="10px" />
-      {filteredPullRequests?.length ? (
-        <PullRequestList>
-          {(filteredPullRequests ?? []).map(
-            (pullRequest: PullRequest, i: number) => {
-              return (
-                <PullRequestRow
-                  onClick={() => {
-                    handlePRRowItemClick(pullRequest);
-                  }}
-                  isLast={i === filteredPullRequests.length - 1}
-                  isSelected={pullRequest === selectedPR}
-                >
-                  <PRName>
-                    <PRIcon src={pr_icon} alt="pull request icon" />
-                    <EllipsisTextWrapper tooltipText={pullRequest.pr_title}>
-                      {pullRequest.pr_title}
-                    </EllipsisTextWrapper>
-                  </PRName>
-
-                  <Flex>
-                    <DeploymentImageContainer>
-                      {/* <InfoWrapper>
-                  <LastDeployed>
-                    #{pullRequest.pr_number} last updated xyz
-                  </LastDeployed>
-                </InfoWrapper>
-                <SepDot>•</SepDot> */}
-                      <MergeInfoWrapper>
-                        <MergeInfo>
-                          {pullRequest.branch_from}
-                          <i className="material-icons">arrow_forward</i>
-                          {pullRequest.branch_into}
-                        </MergeInfo>
-                        <SepDot>•</SepDot>
-                        <LastDeployed>
-                          Last updated{" "}
-                          {dayjs(pullRequest.updated_at).format(
-                            "hh:mma on MM/DD/YYYY"
-                          )}
-                        </LastDeployed>
-                      </MergeInfoWrapper>
-                    </DeploymentImageContainer>
-                  </Flex>
-                </PullRequestRow>
-              );
-            }
-          )}
-        </PullRequestList>
-      ) : (
-        <>
-          <Br height="30px" />
-          <Placeholder height="50vh">{`No pull requests match your search query.`}</Placeholder>
-        </>
-      )}
-      {showErrorsModal && selectedPR ? (
-        <PorterYAMLErrorsModal
-          errors={porterYAMLErrors}
-          onClose={() => setShowErrorsModal(false)}
-          repo={selectedPR.repo_owner + "/" + selectedPR.repo_name}
-          branch={selectedPR.branch_from}
-        />
-      ) : null}
-      {selectedPR && porterYAMLErrors.length ? (
-        <ValidationErrorBannerWrapper>
-          <Banner type="warning">
-            We found some errors in the porter.yaml file in the&nbsp;
-            {selectedPR.branch_from}&nbsp;branch. &nbsp;
-            <LearnMoreButton onClick={() => setShowErrorsModal(true)}>
-              Learn more
-            </LearnMoreButton>
-          </Banner>
-        </ValidationErrorBannerWrapper>
-      ) : null}
-      <CreatePreviewDeploymentWrapper>
-        {showCreatePreviewModal && selectedPR && porterYAMLErrors.length == 0 && (
-          <Modal
-            title="Create Preview Environment"
-            closeModal={() => setShowCreatePreviewModal(false)}
-          >
-            <>
-              <Text color="helper">
-                Create Preview Deployment for {selectedPR.pr_title}?
-              </Text>
-              <Spacer y={1} />
-              {/* <Button
-                onClick={() => handleModalSubmit}
-                loadingText="Submitting..."
-                withBorder
-                status={loading ? "loading" : undefined}
-                disabled={
-                  createPreviewDeploymentMutation.isLoading ||
-                  loading ||
-                  porterYAMLErrors.length > 0
-                }
-              >
-                {createPreviewDeploymentMutation.isLoading
-                  ? "Creating..."
-                  : "Create Preview Deployment"}
-              </Button> */}
-              <SubmitButton
-                onClick={() => createPreviewDeploymentMutation.mutate()}
-                disabled={
-                  loading ||
-                  !selectedPR ||
-                  porterYAMLErrors.length > 0 ||
-                  createPreviewDeploymentMutation.isLoading
-                }
-              >
-                {createPreviewDeploymentMutation.isLoading
-                  ? "Creating..."
-                  : "Create preview deployment"}
-              </SubmitButton>
-            </>
-          </Modal>
-        )}
-        {selectedPR && porterYAMLErrors.length ? (
-          <RevalidatePorterYAMLSpanWrapper>
-            Please fix your porter.yaml file to continue.{" "}
-            <RevalidateSpan
-              onClick={(e) => {
-                e.preventDefault();
-                e.stopPropagation();
-
-                if (!selectedPR) {
-                  return;
-                }
-
-                handlePRRowItemClick(selectedPR);
-              }}
-            >
-              Refresh
-            </RevalidateSpan>
-          </RevalidatePorterYAMLSpanWrapper>
-        ) : null}
-      </CreatePreviewDeploymentWrapper>
-    </>
-  );
-};
-
-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;
-  }
-`;

+ 0 - 305
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx

@@ -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(
-        "<token>",
-        {
-          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 ? (
-        <Modal
-          title={`Remove Preview Envs for ${git_repo_owner}/${git_repo_name}`}
-          width="800px"
-          height="260px"
-          onRequestClose={closeForm}
-        >
-          <Warning highlight>
-            ⚠️ All Preview Environment deployments associated with this repo
-            will be deleted.
-          </Warning>
-          <InputRow
-            type="text"
-            label="Enter the full name of the repository to delete Preview Environments:"
-            value={deleteConfirmationRepoName}
-            placeholder={`${git_repo_owner}/${git_repo_name}`}
-            setValue={(x: string) => setDeleteConfirmationRepoName(x)}
-            width={"500px"}
-          />
-          <ActionWrapper>
-            <DeleteButton
-              onClick={() => handleDelete()}
-              disabled={!canDelete()}
-            >
-              Delete
-            </DeleteButton>
-          </ActionWrapper>
-        </Modal>
-      ) : null} */}
-      <EnvironmentCardWrapper
-        to={`/preview-environments/deployments/${id}/${git_repo_owner}/${git_repo_name}`}
-      >
-        <DataContainer>
-          <RepoName>
-            <Icon
-              src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png"
-              alt="git repository icon"
-            />
-            {git_repo_owner}/{git_repo_name}
-            <RepoLink
-              to={`https://github.com/${git_repo_owner}/${git_repo_name}`}
-              target="_blank"
-              onClick={(e) => {
-                e.preventDefault();
-                e.stopPropagation();
-
-                window.open(
-                  `https://github.com/${git_repo_owner}/${git_repo_name}`,
-                  "_blank"
-                );
-              }}
-            >
-              <i className="material-icons">open_in_new</i>
-              View Repo
-            </RepoLink>
-          </RepoName>
-          <Status>
-            {deployment_count > 0 ? (
-              <>
-                <StatusDot status={last_deployment_status} />
-                Last PR status was "{capitalize(last_deployment_status || "")}"
-                <Dot>•</Dot>
-              </>
-            ) : null}
-            {deployment_count > 0 ? (
-              <Span>
-                {deployment_count || 0} pull{" "}
-                {deployment_count > 1 ? "requests" : "request"} deployed
-              </Span>
-            ) : (
-              <Span>
-                There is no pull request deployed for this environment
-              </Span>
-            )}
-          </Status>
-        </DataContainer>
-      </EnvironmentCardWrapper>
-    </>
-  );
-};
-
-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;
-`;

+ 0 - 493
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentSettings.tsx

@@ -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<boolean>(false);
-  const [availableBranches, setAvailableBranches] = useState<string[]>([]);
-  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<Environment>();
-  const [saveStatus, setSaveStatus] = useState("");
-  const [newCommentsDisabled, setNewCommentsDisabled] = useState(false);
-  const [
-    deploymentMode,
-    setDeploymentMode,
-  ] = useState<EnvironmentDeploymentMode>("manual");
-  const [namespaceLabels, setNamespaceLabels] = useState<KeyValueType[]>([]);
-  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<Environment>(
-        "<token>",
-        {},
-        {
-          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<string[]>(
-        "<token>",
-        {},
-        {
-          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<string, string> = {};
-
-    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(
-        "<token>",
-        {
-          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(
-        "<token>",
-        {
-          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 ? (
-        <DeletePreviewEnvironmentModal
-          repoOwner={repoOwner}
-          repoName={repoName}
-          onClose={closeDeleteConfirmationModal}
-          prompt={deleteConfirmationPrompt}
-          setPrompt={setDeleteConfirmationPrompt}
-          onDelete={handleDelete}
-          disabled={!canDelete}
-        />
-      ) : null}
-      <BreadcrumbRow>
-        <Breadcrumb to={`/preview-environments/deployments/settings`}>
-          <ArrowIcon src={PullRequestIcon} />
-          <Wrap>Preview environments</Wrap>
-        </Breadcrumb>
-        <Slash>/</Slash>
-        <Breadcrumb
-          to={`/preview-environments/deployments/${environmentId}/${selectedRepo}`}
-        >
-          <Icon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
-          <Wrap>{selectedRepo}</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <DashboardHeader
-        image={PullRequestIcon}
-        title="Preview environment settings"
-        description={`Preview environment settings for the ${selectedRepo} repository.`}
-        disableLineBreak
-        capitalize={false}
-      />
-      <WarningBannerWrapper>
-        <Banner type="warning">
-          Changes made here will not affect existing deployments in this preview
-          environment.
-        </Banner>
-      </WarningBannerWrapper>
-      <StyledPlaceholder>
-        <Heading isAtTop>Pull request comment settings</Heading>
-        <Helper>
-          Update the most recent PR comment on every deploy. If disabled, a new
-          PR comment is made per deploy.
-        </Helper>
-        <CheckboxRow
-          label="Update the most recent PR comment"
-          checked={newCommentsDisabled}
-          toggle={() => setNewCommentsDisabled(!newCommentsDisabled)}
-        />
-        <Br />
-        <Heading>Automatic preview deployments</Heading>
-        <Helper>
-          When enabled, preview deployments are automatically created for all
-          new pull requests.
-        </Helper>
-        <CheckboxRow
-          label="Automatically create preview deployments"
-          checked={deploymentMode === "auto"}
-          toggle={() =>
-            setDeploymentMode((deploymentMode) =>
-              deploymentMode === "auto" ? "manual" : "auto"
-            )
-          }
-        />
-        <Br />
-        <Heading>Deploy from branches</Heading>
-        <Helper>
-          Choose the list of branches that you want to deploy changes from.
-        </Helper>
-        <BranchFilterSelector
-          onChange={setDeployBranches}
-          options={availableBranches}
-          value={deployBranches}
-          showLoading={isLoadingBranches}
-        />
-        <Br />
-        <Heading>Select allowed branches</Heading>
-        <Helper>
-          If the pull request has a base branch included in this list, it will
-          be allowed to be deployed.
-          <br />
-          (Leave empty to allow all branches)
-        </Helper>
-        <BranchFilterSelector
-          onChange={setBaseBranches}
-          options={availableBranches}
-          value={baseBranches}
-          showLoading={isLoadingBranches}
-        />
-        <Br />
-        <Heading>Namespace labels</Heading>
-        <Helper>
-          Custom labels to be injected into the Kubernetes namespace created for
-          each deployment.
-        </Helper>
-        <NamespaceLabels
-          values={namespaceLabels}
-          setValues={(x: KeyValueType[]) => {
-            const labels: KeyValueType[] = x.map((entry) => ({
-              key: entry.key,
-              value: entry.value,
-            }));
-
-            setNamespaceLabels(labels);
-          }}
-        />
-        <SavePreviewEnvironmentSettings
-          text={"Save"}
-          status={saveStatus}
-          clearPosition={true}
-          statusPosition={"right"}
-          onClick={handleSave}
-        />
-        <Br />
-        <Heading>Delete preview environment</Heading>
-        <Helper>
-          Delete the Porter preview environment integration for this repo. All
-          preview deployments will also be destroyed.
-        </Helper>
-        <DeleteButton
-          disabled={saveStatus === "loading"}
-          onClick={() => {
-            setShowDeleteModal(true);
-          }}
-        >
-          Delete preview environment
-        </DeleteButton>
-      </StyledPlaceholder>
-    </>
-  );
-};
-
-interface DeletePreviewEnvironmentModalProps {
-  repoName: string;
-  repoOwner: string;
-  prompt: string;
-  setPrompt: (prompt: string) => void;
-  onDelete: () => void;
-  onClose: () => void;
-  disabled: boolean;
-}
-
-const DeletePreviewEnvironmentModal = (
-  props: DeletePreviewEnvironmentModalProps
-) => {
-  return (
-    <Modal
-      height="fit-content"
-      title={`Remove Preview Envs for ${props.repoOwner}/${props.repoName}`}
-      onRequestClose={props.onClose}
-    >
-      <DeletePreviewEnvironmentModalContentsWrapper>
-        <Banner type="warning">
-          All Preview Environment deployments associated with this repo will be
-          deleted.
-        </Banner>
-        <InputRow
-          type="text"
-          label={`Enter ${props.repoOwner}/${props.repoName} to delete Preview Environments:`}
-          value={props.prompt}
-          placeholder={`${props.repoOwner}/${props.repoName}`}
-          setValue={(x: string) => props.setPrompt(x)}
-          width={"500px"}
-        />
-        <Flex justifyContent="center" alignItems="center">
-          <DeleteButton
-            onClick={() => props.onDelete()}
-            disabled={props.disabled}
-          >
-            Delete
-          </DeleteButton>
-        </Flex>
-      </DeletePreviewEnvironmentModalContentsWrapper>
-    </Modal>
-  );
-};
-
-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;
-`;

+ 0 - 184
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx

@@ -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<Environment[]>([]);
-
-  const removeEnvironmentFromList = (deletedEnv: Environment) => {
-    setEnvironments((prev) => {
-      return prev.filter((env) => env.id !== deletedEnv.id);
-    });
-  };
-
-  const getEnvironments = async () => {
-    try {
-      const { data } = await api.listEnvironments(
-        "<token>",
-        {},
-        {
-          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 (
-    <>
-      <PreviewEnvironmentsHeader />
-      <Relative>
-        <ControlRow>
-          <ButtonEnablePREnvironments setIsReady={setButtonIsReady} />
-        </ControlRow>
-        {isLoading ? (
-          <LoadingWrapper>
-            <Loading />
-          </LoadingWrapper>
-        ) : (
-          <>
-            {environments.length === 0 ? (
-              <Placeholder
-                title="No repositories found"
-                height="calc(100vh - 400px)"
-              >
-                No repositories were found with Preview Environments enabled.
-              </Placeholder>
-            ) : (
-              <EnvironmentsGrid>
-                {environments.map((env) => (
-                  <EnvironmentCard
-                    key={env.id}
-                    environment={env}
-                    onDelete={removeEnvironmentFromList}
-                  />
-                ))}
-              </EnvironmentsGrid>
-            )}
-          </>
-        )}
-      </Relative>
-    </>
-  );
-};
-
-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;
-  }
-`;

+ 0 - 3
dashboard/src/main/home/cluster-dashboard/preview-environments/errors.ts

@@ -1,3 +0,0 @@
-export enum PorterYAMLErrors {
-  FileNotFound = "porter.yaml does not exist in the root of this repository",
-}

+ 0 - 177
dashboard/src/main/home/cluster-dashboard/preview-environments/mocks.ts

@@ -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",
-  },
-];

+ 0 - 51
dashboard/src/main/home/cluster-dashboard/preview-environments/routes.tsx

@@ -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 <Redirect to={`/`} />;
-  }
-
-  return (
-    <>
-      <Switch>
-        <Route path={`${path}/connect-repo`}>
-          <ConnectNewRepo />
-        </Route>
-        <Route path={`${path}/details/:id`}>
-          <DeploymentDetail />
-        </Route>
-        <Route
-          path={`${path}/deployments/:environment_id/:repo_owner/:repo_name/settings`}
-        >
-          <EnvironmentSettings />
-        </Route>
-        <Route
-          path={`${path}/deployments/:environment_id/:repo_owner/:repo_name/create`}
-        >
-          <DeployEnvironment />
-        </Route>
-        <Route
-          path={`${path}/deployments/:environment_id/:repo_owner/:repo_name`}
-        >
-          <DeploymentList />
-        </Route>
-        <Route path={`${path}/`}>
-          <EnvironmentsList />
-        </Route>
-      </Switch>
-    </>
-  );
-};
-
-export default Routes;

+ 0 - 50
dashboard/src/main/home/cluster-dashboard/preview-environments/utils.ts

@@ -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(
-    "<token>",
-    {
-      ...(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(
-    "<token>",
-    {
-      environment_id: environmentID,
-    },
-    {
-      project_id: projectID,
-      cluster_id: clusterID,
-    }
-  );
-};

+ 0 - 156
dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx

@@ -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 (
-    <>
-      <DashboardHeader
-        materialIconClass="material-icons-outlined"
-        image={"lan"}
-        title="Stacks"
-        description="Groups of applications deployed from a shared source."
-        disableLineBreak
-      />
-      <ControlRow>
-        <FilterWrapper>
-          <NamespaceSelector
-            namespace={currentNamespace}
-            setNamespace={handleNamespaceChange}
-          />
-        </FilterWrapper>
-        <Flex>
-          <RadioFilter
-            selected={currentSort}
-            noMargin
-            dropdownAlignRight={false}
-            setSelected={(sortType: any) => 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}
-          />
-          <Button to={"/stacks/launch"}>
-            <i className="material-icons">add</i>
-            Create stack
-          </Button>
-        </Flex>
-      </ControlRow>
-      <StackList namespace={currentNamespace} sortBy={currentSort} />
-    </>
-  );
-};
-
-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;
-  }
-`;

+ 0 - 358
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx

@@ -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<FullStackRevision>(
-    () => stack.latest_revision
-  );
-
-  const handleDelete = () => {
-    setIsDeleting(true);
-    api
-      .deleteStack(
-        "<token>",
-        {},
-        {
-          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 (
-      <Placeholder height="400px">
-        <div>
-          <h1>Deleting Stack</h1>
-          <p>This may take some time...</p>
-          <Loading />
-        </div>
-      </Placeholder>
-    );
-  }
-
-  return (
-    <div>
-      <BreadcrumbRow>
-        <Breadcrumb to="/stacks">
-          <ArrowIcon src={leftArrow} />
-          <Wrap>Back</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <StackTitleWrapper>
-        <TitleSection materialIconClass="material-icons-outlined" icon={"lan"}>
-          {stack.name}
-        </TitleSection>
-        <NamespaceTag.Wrapper>
-          Namespace
-          <NamespaceTag.Tag>{stack.namespace}</NamespaceTag.Tag>
-        </NamespaceTag.Wrapper>
-      </StackTitleWrapper>
-
-      {/* Stack error message */}
-      {currentRevision &&
-      currentRevision?.reason &&
-      currentRevision?.message?.length > 0 ? (
-        <StackErrorMessageStyles.Wrapper>
-          <i className="material-icons">history</i>
-          <StackErrorMessageStyles.Text color="#aaaabb">
-            {currentRevision?.status === "failed" ? "Error: " : ""}
-            {currentRevision?.message}
-          </StackErrorMessageStyles.Text>
-        </StackErrorMessageStyles.Wrapper>
-      ) : null}
-
-      <Break />
-      <InfoWrapper>
-        <LastDeployed>
-          <Status
-            status={getStackStatus(stack)}
-            message={getStackStatusMessage(stack)}
-          />
-          <SepDot>•</SepDot>
-          Last updated {readableDate(stack.updated_at)}
-        </LastDeployed>
-      </InfoWrapper>
-
-      <RevisionList
-        revisions={stack.revisions}
-        currentRevision={currentRevision}
-        latestRevision={stack.latest_revision}
-        stackId={stack.id}
-        stackNamespace={namespace}
-        onRevisionClick={(revision) => setCurrentRevision(revision)}
-        onRollback={() => refreshStack()}
-      ></RevisionList>
-      <Br />
-      <TabSelector
-        currentTab={currentTab}
-        options={[
-          {
-            label: "Apps",
-            value: "apps",
-            component: (
-              <>
-                <Gap></Gap>
-                <Action.Row>
-                  <Action.Button to={`${url}/new-app-resource`}>
-                    <i className="material-icons">add</i>
-                    Create app resource
-                  </Action.Button>
-                </Action.Row>
-                {currentRevision.id !== stack.latest_revision.id ? (
-                  <ChartListWrapper>
-                    <Placeholder>
-                      Not available when previewing revisions
-                    </Placeholder>
-                  </ChartListWrapper>
-                ) : (
-                  <ChartListWrapper>
-                    <ChartList
-                      currentCluster={currentCluster}
-                      currentView="stacks"
-                      namespace={namespace}
-                      sortType="Alphabetical"
-                      appFilters={
-                        stack?.latest_revision?.resources?.map(
-                          (res) => res.name
-                        ) || []
-                      }
-                      closeChartRedirectUrl={`${window.location.pathname}${window.location.search}`}
-                    />
-                  </ChartListWrapper>
-                )}
-              </>
-            ),
-          },
-          {
-            label: "Source config",
-            value: "source_config",
-            component: (
-              <>
-                <SourceConfig
-                  namespace={namespace}
-                  revision={currentRevision}
-                  readOnly={stack.latest_revision.id !== currentRevision.id}
-                  onSourceConfigUpdate={() => refreshStack()}
-                ></SourceConfig>
-              </>
-            ),
-          },
-          {
-            label: "Env groups",
-            value: "env_groups",
-            component: (
-              <>
-                <Gap></Gap>
-                <Action.Row>
-                  <Action.Button to={`${url}/new-env-group`}>
-                    <i className="material-icons">add</i>
-                    Create env group
-                  </Action.Button>
-                </Action.Row>
-                <EnvGroups stack={stack} />
-              </>
-            ),
-          },
-          {
-            label: "Settings",
-            value: "settings",
-            component: (
-              <>
-                <Gap></Gap>
-                <Settings
-                  stack={stack}
-                  onDelete={handleDelete}
-                  onUpdate={refreshStack}
-                />
-              </>
-            ),
-          },
-        ]}
-        setCurrentTab={(tab) => {
-          setCurrentTab(tab);
-        }}
-      ></TabSelector>
-      <PaddingBottom />
-    </div>
-  );
-};
-
-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;
-  }
-`;

+ 0 - 160
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_Settings.tsx

@@ -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<string, string>
-    );
-  const secret_variables = Object.entries(envGroup.variables)
-    .filter(([_, value]) => value.includes("PORTERSECRET"))
-    .reduce(
-      (acc, [key, value]) => ({ ...acc, [key]: value }),
-      {} as Record<string, string>
-    );
-
-  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<string, string>;
-      secret_variables: Record<string, string>;
-      linked_applications: string[];
-    }[]
-  >([]);
-
-  const { pushFiltered } = useRouting();
-
-  const populateEnvGroups = async () => {
-    const stackEnvGroups = stack.latest_revision.env_groups;
-    const envGroupsPromises = stackEnvGroups.map((envGroup) =>
-      api
-        .getEnvGroup<PopulatedEnvGroup>(
-          "<token>",
-          {},
-          {
-            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(
-        "<token>",
-        {
-          ...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 (
-    <NewAppResourceForm
-      availableEnvGroups={availableEnvGroups}
-      namespace={stack.namespace}
-      sourceConfig={stack.latest_revision.source_configs[0]}
-      templateInfo={{
-        name: params.template_name,
-        version: params.template_version,
-      }}
-      onCancel={() => {
-        pushFiltered(`../template-selector`, []);
-      }}
-      onSubmit={handleSubmit}
-    />
-  );
-};
-
-export default Settings;

+ 0 - 162
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_TemplateSelector.tsx

@@ -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<PorterTemplate[]>([]);
-  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<PorterTemplate[]>(
-        "<token>",
-        {
-          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<string, string>);
-
-        if (isSubscribed) {
-          setTemplates(porterTemplates);
-          setSelectedVersion(latestVersions);
-        }
-      })
-      .catch(() => {
-        if (isSubscribed) {
-          setHasError(true);
-        }
-      })
-      .finally(() => {
-        if (isSubscribed) {
-          setIsLoading(false);
-        }
-      });
-
-    return () => {
-      isSubscribed = false;
-    };
-  }, []);
-
-  if (isLoading) {
-    return <Loading />;
-  }
-
-  if (hasError) {
-    return (
-      <Placeholder>
-        <div>
-          <h2>Unexpected error</h2>
-          <p>
-            We had an error retrieving the available templates, please try
-            again.
-          </p>
-        </div>
-      </Placeholder>
-    );
-  }
-
-  return (
-    <>
-      <TitleSection>
-        <DynamicLink to={`../`}>
-          <BackButton>
-            <i className="material-icons">keyboard_backspace</i>
-          </BackButton>
-        </DynamicLink>
-        Select a template
-      </TitleSection>
-      <Card.Grid>
-        {templates.map((template) => {
-          return (
-            <Card.Wrapper
-              key={template.name}
-              as={DynamicLink}
-              to={`settings/${template.name}/${selectedVersion[template.name]}`}
-            >
-              <Card.Title>
-                New {template.name} with version:
-                <div
-                  onClickCapture={(e) => {
-                    e.preventDefault();
-                  }}
-                >
-                  <VersionSelector
-                    value={selectedVersion[template.name]}
-                    options={template.versions}
-                    onChange={(newVersion) => {
-                      setSelectedVersion((prev) => ({
-                        ...prev,
-                        [template.name]: newVersion,
-                      }));
-                    }}
-                  />
-                </div>
-              </Card.Title>
-              <Card.Actions>
-                <i className="material-icons-outlined">arrow_forward</i>
-              </Card.Actions>
-            </Card.Wrapper>
-          );
-        })}
-      </Card.Grid>
-    </>
-  );
-};
-
-export default TemplateSelector;

+ 0 - 27
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/index.tsx

@@ -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 (
-    <Switch>
-      <Route path={`${url}/template-selector`}>
-        <TemplateSelector />
-      </Route>
-      <Route path={`${url}/settings/:template_name/:template_version`}>
-        <Settings />
-      </Route>
-      <Route path="/">
-        <Redirect to={`${url}/template-selector`} />
-      </Route>
-      <Route path="*">
-        <Redirect to={url} />
-      </Route>
-    </Switch>
-  );
-};
-
-export default NewAppResourceRoutes;

+ 0 - 66
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewEnvGroup.tsx

@@ -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(
-        "<token>",
-        {
-          ...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 (
-    <>
-      <NewEnvGroupForm
-        onSubmit={createEnvGroup}
-        onCancel={() => {
-          pushFiltered("../" + stack.id, []);
-        }}
-      />
-    </>
-  );
-};
-
-export default NewEnvGroup;

+ 0 - 99
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/Store.tsx

@@ -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<void>;
-}
-
-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<Stack>(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<Stack>(
-        "<token>",
-        {},
-        {
-          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 (
-      <Placeholder>
-        <Loading />
-      </Placeholder>
-    );
-  }
-
-  return (
-    <ExpandedStackStore.Provider
-      value={{
-        stack,
-        refreshStack: async () => {
-          await getStack({ subscribed: true });
-        },
-      }}
-    >
-      {children}
-    </ExpandedStackStore.Provider>
-  );
-};
-
-export default ExpandedStackStoreProvider;

+ 0 - 320
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_RevisionList.tsx

@@ -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<FullStackRevision>(
-        "<token>",
-        {},
-        {
-          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(
-        "<token>",
-        {
-          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 <div>No revisions</div>;
-    }
-
-    return revisions.map((revision, i) => {
-      let isCurrent = latestRevision.id === revision.id;
-      return (
-        <Tr
-          key={i}
-          onClick={() => handleRevisionPreview(revision)}
-          selected={currentRevision.id === revision.id}
-        >
-          <Td>{revision.id}</Td>
-          <Td>{readableDate(revision.created_at)}</Td>
-          <Td>
-            <RevisionStatusWrapper status={revision.status}>
-              {revision.status}
-            </RevisionStatusWrapper>
-          </Td>
-          <Td>
-            <RollbackButton
-              disabled={isCurrent}
-              onClick={(e) => {
-                e.stopPropagation();
-                handleRevisionRollback(revision);
-              }}
-            >
-              {isCurrent ? "Current" : "Revert"}
-            </RollbackButton>
-          </Td>
-        </Tr>
-      );
-    });
-  };
-
-  return (
-    <>
-      <StyledRevisionSection showRevisions={isExpanded}>
-        {isLoading ? (
-          <LoadingOverlay>
-            <Loading />
-          </LoadingOverlay>
-        ) : null}
-        <RevisionHeader
-          showRevisions={isExpanded}
-          isCurrent={currentRevision.id === latestRevision.id}
-          onClick={() => setIsExpanded((prev) => !prev)}
-        >
-          <RevisionPreview>
-            {currentRevision.id === latestRevision.id
-              ? `Current version`
-              : `Previewing revision (not deployed)`}{" "}
-            - <Revision>No. {currentRevision.id}</Revision>
-            <i className="material-icons">arrow_drop_down</i>
-          </RevisionPreview>
-        </RevisionHeader>
-        <TableWrapper>
-          <RevisionsTable>
-            <tbody>
-              <Tr disableHover={true}>
-                <Th>Revision No.</Th>
-                <Th>Timestamp</Th>
-                <Th>Status</Th>
-                <Th>Rollback</Th>
-              </Tr>
-              {revisionList()}
-            </tbody>
-          </RevisionsTable>
-        </TableWrapper>
-      </StyledRevisionSection>
-    </>
-  );
-};
-
-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;
-`;

+ 0 - 222
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/_SourceConfig.tsx

@@ -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(
-        "<token>",
-        {
-          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 (
-    <SourceConfigStyles.Wrapper>
-      {revision.source_configs.map((sourceConfig) => {
-        return (
-          <SourceConfigItem
-            sourceConfig={sourceConfig}
-            key={sourceConfig.id}
-            handleChange={handleChange}
-            disabled={readOnly || buttonStatus === "loading"}
-          />
-        );
-      })}
-      {readOnly ? null : (
-        <SourceConfigStyles.SaveButtonRow>
-          <SourceConfigStyles.SaveButton
-            onClick={handleSave}
-            text="Save"
-            clearPosition={true}
-            makeFlush={true}
-            status={buttonStatus}
-            statusPosition="left"
-          />
-        </SourceConfigStyles.SaveButtonRow>
-      )}
-    </SourceConfigStyles.Wrapper>
-  );
-};
-
-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 (
-    <SourceConfigStyles.ItemContainer>
-      {editNameMode && !disabled ? (
-        <>
-          <SourceConfigStyles.ItemTitle>
-            <PlainTextInput
-              value={name}
-              onChange={(e) => handleNameChange(e.target.value)}
-              type="text"
-              disabled={disabled}
-            />
-            <EditButton onClick={handleNameChangeCancel}>
-              <i className="material-icons-outlined">close</i>
-            </EditButton>
-          </SourceConfigStyles.ItemTitle>
-        </>
-      ) : (
-        <SourceConfigStyles.ItemTitle>
-          <span>{name}</span>
-
-          <EditButton onClick={toggleEditNameMode}>
-            <i className="material-icons-outlined">edit</i>
-          </EditButton>
-        </SourceConfigStyles.ItemTitle>
-      )}
-
-      <SourceEditorDocker
-        sourceConfig={sourceConfig}
-        onChange={handleChange}
-        readOnly={disabled}
-      />
-    </SourceConfigStyles.ItemContainer>
-  );
-};
-
-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;
-`;

+ 0 - 116
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/EnvGroups.tsx

@@ -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<string, string>;
-  version: number;
-};
-
-const EnvGroups = ({ stack }: { stack: Stack }) => {
-  const { currentProject, currentCluster } = useContext(Context);
-  const [isLoading, setIsLoading] = useState(true);
-  const [envGroups, setEnvGroups] = useState<PopulatedEnvGroup[]>([]);
-  const { url } = useRouteMatch();
-
-  const getEnvGroups = async () => {
-    const stackEnvGroups = stack.latest_revision.env_groups;
-    return Promise.all(
-      stackEnvGroups.map((envGroup) =>
-        api
-          .getEnvGroup<PopulatedEnvGroup>(
-            "<token>",
-            {},
-            {
-              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 (
-      <Placeholder height="250px">
-        <Loading />
-      </Placeholder>
-    );
-  }
-
-  if (envGroups.length === 0) {
-    return (
-      <Placeholder height="250px">
-        <div>
-          <h3>No environment groups found for this stack</h3>
-        </div>
-      </Placeholder>
-    );
-  }
-
-  return (
-    <>
-      <Card.Grid style={{ marginTop: "0px" }}>
-        {envGroups.map((envGroup) => {
-          return (
-            <Card.Wrapper variant="unclickable">
-              <Card.Title>
-                <Card.SmallerIcon src={sliders}></Card.SmallerIcon>
-                {envGroup.name}
-              </Card.Title>
-
-              <Card.Actions>
-                <Card.ActionButton
-                  as={DynamicLink}
-                  to={{
-                    pathname: "/env-groups",
-                    search: `?namespace=${stack.namespace}&selected_env_group=${
-                      envGroup.name
-                    }&redirect_url=${encodeURIComponent(
-                      window.location.pathname
-                    )}`,
-                  }}
-                >
-                  <i className="material-icons-outlined">launch</i>
-                </Card.ActionButton>
-              </Card.Actions>
-            </Card.Wrapper>
-          );
-        })}
-      </Card.Grid>
-    </>
-  );
-};
-
-export default EnvGroups;

+ 0 - 259
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx

@@ -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<T> = {
-  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 = <T extends unknown>({
-  value,
-  options,
-  accessor,
-  onChange,
-  isOptionEqualToValue,
-  label,
-  isLoading,
-  placeholder,
-  dropdown,
-  className,
-  readOnly,
-}: SelectProps<T>) => {
-  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 <SelectStyles.Label> {label} </SelectStyles.Label>;
-    }
-    return null;
-  };
-
-  if (isLoading) {
-    return (
-      <div>
-        {getLabel()}
-        <SelectStyles.Wrapper>
-          <SelectStyles.Selector
-            className={className}
-            expanded={false}
-            readOnly={readOnly}
-          >
-            <SelectStyles.Loading>
-              <Loading />
-            </SelectStyles.Loading>
-          </SelectStyles.Selector>
-        </SelectStyles.Wrapper>
-      </div>
-    );
-  }
-
-  const isSelected = (option: T, value: T) => {
-    if (!value) {
-      return false;
-    }
-
-    if (isOptionEqualToValue) {
-      return isOptionEqualToValue(option, value);
-    }
-  };
-
-  return (
-    <div>
-      {getLabel()}
-      <SelectStyles.Wrapper ref={wrapperRef}>
-        <SelectStyles.Selector
-          className={className}
-          onClick={() => setExpanded(!expanded)}
-          expanded={!readOnly && expanded}
-          readOnly={readOnly}
-        >
-          <SelectStyles.CurrentValue>
-            <span>{value ? accessor(value) : placeholder}</span>
-          </SelectStyles.CurrentValue>
-          {readOnly ? null : <i className="material-icons">arrow_drop_down</i>}
-        </SelectStyles.Selector>
-        {expanded && !readOnly ? (
-          <SelectStyles.Dropdown.Wrapper
-            width={dropdown?.width}
-            maxH={dropdown?.maxH}
-          >
-            {dropdown?.label && (
-              <SelectStyles.Dropdown.Label>
-                {dropdown?.label}
-              </SelectStyles.Dropdown.Label>
-            )}
-            {options.length > 0 ? (
-              <>
-                {options.map((option, i) => (
-                  <SelectStyles.Dropdown.Option
-                    key={i}
-                    onClick={() => !readOnly && handleOptionClick(option)}
-                    lastItem={i === options.length - 1}
-                    selected={isSelected(option, value)}
-                    height={dropdown?.option?.height}
-                  >
-                    {accessor(option)}
-                  </SelectStyles.Dropdown.Option>
-                ))}
-              </>
-            ) : (
-              <SelectStyles.Dropdown.NoOptions>
-                No options available
-              </SelectStyles.Dropdown.NoOptions>
-            )}
-          </SelectStyles.Dropdown.Wrapper>
-        ) : null}
-      </SelectStyles.Wrapper>
-    </div>
-  );
-};
-
-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;
-      }
-    `,
-  },
-};

+ 0 - 139
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Settings.tsx

@@ -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<void>;
-}) => {
-  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(
-        "<token>",
-        {
-          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 (
-    <Wrapper>
-      <StyledSettingsSection>
-        <Heading>Update Stack name</Heading>
-
-        <InputRow
-          label="Stack name"
-          value={stackName}
-          setValue={setStackName as any}
-          type="text"
-          width="300px"
-        />
-        <SaveButton
-          text="Update"
-          onClick={handleStackNameChange}
-          disabled={stackName === stack.name}
-          makeFlush
-          clearPosition
-          statusPosition="right"
-          status={buttonStatus}
-        ></SaveButton>
-
-        <Heading>Additional Settings</Heading>
-
-        <Button color="#b91133" onClick={handleDelete}>
-          Delete stack
-        </Button>
-      </StyledSettingsSection>
-    </Wrapper>
-  );
-};
-
-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%)" : "")};
-  }
-`;

+ 0 - 328
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/SourceEditorDocker.tsx

@@ -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<DockerRegistry | null>(null);
-  const [image, setImage] = useState<string | null>(
-    () => sourceConfig.image_repo_uri
-  );
-  const [tag, setTag] = useState<string | null>(() => 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 (
-    <>
-      <SourceEditorDockerStlyes.RegistryWrapper>
-        <_DockerRepositorySelector
-          currentImageUrl={sourceConfig.image_repo_uri}
-          value={registry}
-          onChange={setRegistry}
-          readOnly={readOnly}
-        />
-      </SourceEditorDockerStlyes.RegistryWrapper>
-      {registry && (
-        <SourceEditorDockerStlyes.ImageAndTagWrapper>
-          <_ImageSelector
-            registry={registry}
-            value={image}
-            onChange={setImage}
-            readOnly={readOnly}
-          />
-
-          {registry && imageName && (
-            <_TagSelector
-              registry={registry}
-              imageName={imageName}
-              value={tag}
-              onChange={setTag}
-              readOnly={readOnly}
-            />
-          )}
-        </SourceEditorDockerStlyes.ImageAndTagWrapper>
-      )}
-    </>
-  );
-};
-
-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<DockerRegistry[]>([]);
-  const [isLoading, setIsLoading] = useState(true);
-
-  useEffect(() => {
-    api
-      .getProjectRegistries<DockerRegistry[]>(
-        "<token>",
-        {},
-        {
-          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 (
-    <>
-      <Select
-        value={value}
-        options={registries}
-        onChange={handleChange}
-        accessor={(val) => val.name}
-        label="Docker Registry"
-        placeholder="Select a registry"
-        isOptionEqualToValue={(a, b) => a?.url === b?.url}
-        readOnly={readOnly}
-        isLoading={isLoading}
-        dropdown={{
-          maxH: "200px",
-        }}
-      />
-    </>
-  );
-};
-
-type ImageRepo = {
-  name: string;
-  created_at: string;
-  uri: string;
-};
-
-const _ImageSelector = ({
-  registry,
-  value,
-  onChange,
-  readOnly,
-}: {
-  registry: DockerRegistry;
-  value: string;
-  onChange: (newValue: string) => void;
-  readOnly: boolean;
-}) => {
-  const { currentProject } = useContext(Context);
-
-  const [images, setImages] = useState<ImageRepo[]>([]);
-  const [isLoading, setIsLoading] = useState(true);
-
-  useEffect(() => {
-    setIsLoading(true);
-    api
-      .getImageRepos<ImageRepo[]>(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-          registry_id: registry.id,
-        }
-      )
-      .then(({ data }) => {
-        setImages(data);
-
-        if (!value) {
-          onChange(data[0].uri);
-        }
-        setIsLoading(false);
-      });
-  }, []);
-
-  const handleChange = (image: string) => {
-    onChange(image);
-  };
-
-  const displayName = (imageUrl: string) => {
-    const image = images.find((i) => i.uri === imageUrl);
-    if (!image) {
-      return imageUrl;
-    }
-    return image.name;
-  };
-
-  return (
-    <Select
-      value={value}
-      options={images.map((image) => 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<string[]>([]);
-  const [isLoading, setIsLoading] = useState(true);
-
-  useEffect(() => {
-    setIsLoading(true);
-    api
-      .getImageTags<DockerImageTag[]>(
-        "<token>",
-        {},
-        {
-          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 (
-    <Select
-      value={value}
-      options={imageTags}
-      accessor={(tag) => tag}
-      label="Tag"
-      placeholder="Select a tag"
-      onChange={handleChange}
-      readOnly={readOnly}
-      isLoading={isLoading}
-      dropdown={{
-        maxH: "200px",
-      }}
-    />
-  );
-};
-
-export default SourceEditorDocker;
-
-const SourceEditorDockerStlyes = {
-  RegistryWrapper: styled.div``,
-  ImageAndTagWrapper: styled.div`
-    display: grid;
-    grid-template-columns: 3fr 1fr;
-    grid-gap: 10px;
-    align-items: center;
-  `,
-};

+ 0 - 63
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/routes.tsx

@@ -1,63 +0,0 @@
-import React from "react";
-import {
-  Redirect,
-  Route,
-  Switch,
-  useLocation,
-  useRouteMatch,
-} from "react-router";
-import styled from "styled-components";
-
-import ExpandedStack from "./ExpandedStack";
-import NewAppResourceRoutes from "./NewAppResource";
-import NewEnvGroup from "./NewEnvGroup";
-import ExpandedStackStoreProvider from "./Store";
-
-const ExpandedStackRoutes = () => {
-  const { path } = useRouteMatch();
-  const { pathname } = useLocation();
-
-  return (
-    <ExpandedStackStoreProvider>
-      <Switch>
-        <Redirect from="/:url*(/+)" to={pathname.slice(0, -1)} />
-        <Route path={`${path}/new-env-group`} exact>
-          <StyledLaunchFlow>
-            <LaunchContainer>
-              <NewEnvGroup />
-            </LaunchContainer>
-          </StyledLaunchFlow>
-        </Route>
-        <Route path={`${path}/new-app-resource`}>
-          <StyledLaunchFlow>
-            <LaunchContainer>
-              <NewAppResourceRoutes />
-            </LaunchContainer>
-          </StyledLaunchFlow>
-        </Route>
-        <Route path={`${path}`} exact>
-          <ExpandedStack />
-        </Route>
-        <Route path={`*`}>
-          <div>Not found</div>
-        </Route>
-      </Switch>
-    </ExpandedStackStoreProvider>
-  );
-};
-
-export default ExpandedStackRoutes;
-
-const LaunchContainer = styled.div`
-  margin: 0 auto;
-  width: 100%;
-`;
-
-const StyledLaunchFlow = styled.div`
-  width: calc(100% - 100px);
-  margin-left: 50px;
-  min-width: 300px;
-  margin-top: ${(props: { disableMarginTop?: boolean }) =>
-    props.disableMarginTop ? "inherit" : "calc(50vh - 380px)"};
-  margin-bottom: 50px;
-`;

+ 0 - 264
dashboard/src/main/home/cluster-dashboard/stacks/_StackList.tsx

@@ -1,264 +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 { Context } from "shared/Context";
-import Placeholder from "components/OldPlaceholder";
-import styled from "styled-components";
-import { Stack } from "./types";
-import { readableDate } from "shared/string_utils";
-import { Card } from "./launch/components/styles";
-import Status, { StatusProps } from "./components/Status";
-import {
-  Flex,
-  InfoWrapper,
-  LastDeployed,
-  NamespaceTag,
-  SepDot,
-  Text,
-} from "./components/styles";
-import { getStackStatus, getStackStatusMessage } from "./shared";
-
-const StackList = ({
-  namespace,
-  sortBy,
-}: {
-  namespace: string;
-  sortBy: "created_at" | "updated_at" | "alphabetical";
-}) => {
-  const { currentProject, currentCluster, setCurrentError } = useContext(
-    Context
-  );
-  const [stacks, setStacks] = useState<Stack[]>(null);
-  const [isLoading, setIsLoading] = useState(true);
-  const [deleting, setDeleting] = useState<string | null>(null);
-
-  const handleDelete = (stack: Stack) => {
-    setDeleting(stack.id);
-    api
-      .deleteStack(
-        "<token>",
-        {},
-        {
-          namespace,
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-          stack_id: stack.id,
-        }
-      )
-      .then(() => {
-        setStacks((prev) => prev.filter((s) => s.id !== stack.id));
-      })
-      .catch((err) => {
-        setCurrentError(err);
-      })
-      .finally(() => {
-        setDeleting(null);
-      });
-  };
-
-  useEffect(() => {
-    let isSubscribed = true;
-
-    setIsLoading(true);
-
-    api
-      .listStacks(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-          cluster_id: currentCluster.id,
-          namespace,
-        }
-      )
-      .then((res) => {
-        if (isSubscribed) {
-          setStacks(res.data);
-        }
-      })
-      .catch((err) => {
-        if (isSubscribed) {
-          setCurrentError(err);
-        }
-      })
-      .finally(() => {
-        if (isSubscribed) {
-          setIsLoading(false);
-        }
-      });
-
-    return () => {
-      isSubscribed = false;
-    };
-  }, [namespace, currentCluster]);
-
-  const sortedStacks = useMemo(() => {
-    return (
-      stacks?.sort((a, b) => {
-        switch (sortBy) {
-          case "created_at":
-            return Date.parse(a.created_at) < Date.parse(b.created_at) ? 1 : -1;
-          case "updated_at":
-            return Date.parse(a.updated_at) < Date.parse(b.updated_at) ? 1 : -1;
-          default:
-            return a.name > b.name ? 1 : -1;
-        }
-      }) || []
-    );
-  }, [stacks, sortBy]);
-
-  if (isLoading) {
-    return <Loading />;
-  }
-
-  if (stacks?.length === 0) {
-    return (
-      <Placeholder height="250px">
-        <div>
-          <h3>No stacks found</h3>
-          <p>You can create a stack by clicking the "Create stack" button.</p>
-        </div>
-      </Placeholder>
-    );
-  }
-
-  if (sortedStacks.length === 0) {
-    return (
-      <Placeholder height="250px">
-        <div>
-          <h3>No stacks found with the given filters</h3>
-        </div>
-      </Placeholder>
-    );
-  }
-
-  return (
-    <>
-      <Card.Grid>
-        {stacks.map((stack) => (
-          <StackCard
-            as={DynamicLink}
-            key={stack?.id}
-            to={`/stacks/${stack?.namespace}/${stack?.id}`}
-          >
-            <DataContainer>
-              <Top>
-                <StackName>
-                  <StackIcon>
-                    <i className="material-icons-outlined">lan</i>
-                  </StackIcon>
-                  <span>{stack.name}</span>
-                </StackName>
-                <SepDot>•</SepDot>
-                <NamespaceTag.Wrapper>
-                  Namespace
-                  <NamespaceTag.Tag>{stack.namespace}</NamespaceTag.Tag>
-                </NamespaceTag.Wrapper>
-              </Top>
-              <InfoWrapper>
-                <LastDeployed>
-                  <Status
-                    status={getStackStatus(stack)}
-                    message={getStackStatusMessage(stack)}
-                  />
-                  <SepDot>•</SepDot>
-                  <Text color="#aaaabb">
-                    {!stack.latest_revision?.id
-                      ? `No version found`
-                      : `v${stack.latest_revision.id}`}
-                  </Text>
-                  <SepDot>•</SepDot>
-                  Last updated {readableDate(stack.updated_at)}
-                </LastDeployed>
-              </InfoWrapper>
-            </DataContainer>
-            <Flex>
-              <RowButton
-                onClick={(e) => {
-                  e.preventDefault();
-                  e.stopPropagation();
-                  handleDelete(stack);
-                }}
-                disabled={
-                  deleting === stack.id || (deleting && deleting === stack.id)
-                }
-              >
-                <i className="material-icons">delete</i>
-
-                {deleting === stack.id ? <Loading /> : "Delete"}
-              </RowButton>
-            </Flex>
-          </StackCard>
-        ))}
-      </Card.Grid>
-    </>
-  );
-};
-
-export default StackList;
-
-const RowButton = styled.button`
-  min-width: 82px;
-  white-space: nowrap;
-  font-size: 12px;
-  padding: 8px 10px;
-  font-weight: 400;
-  height: 32px;
-  margin-right: 5px;
-  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 StackIcon = styled.div`
-  margin-bottom: -4px;
-
-  > i {
-    font-size: 18px;
-    margin-left: -1px;
-    margin-right: 9px;
-    color: #ffffff66;
-  }
-`;
-
-const StackName = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  color: #ffffff;
-  display: flex;
-  font-size: 14px;
-  align-items: center;
-`;
-
-const DataContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  justify-content: space-between;
-  max-width: calc(100% - 100px);
-  overflow: hidden;
-`;
-
-const StackCard = styled(Card.Wrapper)`
-  font-size: 13px;
-  font-weight: 500;
-`;
-
-const Top = styled.div`
-  display: flex;
-  align-items: center;
-  margin-bottom: 10px;
-`;

+ 0 - 316
dashboard/src/main/home/cluster-dashboard/stacks/components/NewAppResourceForm.tsx

@@ -1,316 +0,0 @@
-import Loading from "components/Loading";
-import { PopulatedEnvGroup } from "components/porter-form/types";
-import TitleSection from "components/TitleSection";
-import _ from "lodash";
-import React, { useContext, useEffect, useState } from "react";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { useRouting } from "shared/routing";
-import { ExpandedPorterTemplate } from "shared/types";
-import styled from "styled-components";
-import { BackButton, Icon, Polymer } from "../launch/components/styles";
-import { CreateStackBody, SourceConfig } from "../types";
-import { hardcodedIcons } from "shared/hardcodedNameDict";
-import Heading from "components/form-components/Heading";
-import InputRow from "components/form-components/InputRow";
-import Helper from "components/form-components/Helper";
-import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
-
-const parseEnvGroup = (namespace: string) => (
-  envGroup: CreateStackBody["env_groups"][0]
-): PopulatedEnvGroup => {
-  const variables = envGroup?.variables || {};
-  const secretVariables = envGroup?.secret_variables || {};
-
-  return {
-    name: envGroup.name,
-    version: 1,
-    namespace,
-    applications: envGroup.linked_applications,
-    meta_version: 2,
-    variables: {
-      ...variables,
-      ...Object.keys(secretVariables).reduce((acc, key) => {
-        acc[key] = "PORTERSECRET_" + key;
-        return acc;
-      }, {} as any),
-    },
-  };
-};
-
-const NewAppResourceForm = (props: {
-  templateInfo: {
-    name: string;
-    version: string;
-  };
-  namespace: string;
-  sourceConfig: Pick<
-    SourceConfig,
-    "build" | "image_repo_uri" | "image_tag" | "name"
-  >;
-  availableEnvGroups: CreateStackBody["env_groups"];
-  onSubmit: (
-    newApp: CreateStackBody["app_resources"][0],
-    syncedEnvGroups: string[]
-  ) => Promise<void>;
-  onCancel: () => void;
-}) => {
-  const {
-    availableEnvGroups,
-    sourceConfig,
-    templateInfo,
-    namespace,
-    onCancel,
-    onSubmit,
-  } = props;
-
-  const { currentProject, currentCluster } = useContext(Context);
-
-  const [hasError, setHasError] = useState(false);
-  const [isLoading, setIsLoading] = useState(true);
-  const [template, setTemplate] = useState<ExpandedPorterTemplate>();
-  const [saveButtonStatus, setSaveButtonStatus] = useState("");
-
-  const [name, setName] = useState("");
-
-  const { pushFiltered } = useRouting();
-
-  const handleSubmit = async ({
-    values: rawValues,
-    metadata,
-  }: {
-    values: any;
-    metadata: any;
-  }) => {
-    setSaveButtonStatus("loading");
-    const syncedEnvGroups =
-      metadata["container.env"]?.added?.map(
-        ({ name }: { name: string }) => name
-      ) || [];
-
-    // Convert dotted keys to nested objects
-    let values: any = {};
-    for (let key in rawValues) {
-      _.set(values, key, rawValues[key]);
-    }
-
-    const stackSourceConfig = sourceConfig;
-    if (!stackSourceConfig) {
-      return;
-    }
-
-    let url = stackSourceConfig.image_repo_uri;
-    let tag = stackSourceConfig.image_tag;
-
-    if (url?.includes(":")) {
-      let splits = url.split(":");
-      url = splits[0];
-      tag = splits[1];
-    } else if (!tag) {
-      tag = "latest";
-    }
-
-    if (!_.isEmpty(stackSourceConfig.build)) {
-      if (template?.metadata?.name === "job") {
-        url = "public.ecr.aws/o1j4x7p4/hello-porter-job";
-        tag = "latest";
-      } else {
-        url = "public.ecr.aws/o1j4x7p4/hello-porter";
-        tag = "latest";
-      }
-    }
-
-    let provider;
-    switch (currentCluster.service) {
-      case "eks":
-        provider = "aws";
-        break;
-      case "gke":
-        provider = "gcp";
-        break;
-      case "doks":
-        provider = "digitalocean";
-        break;
-      case "aks":
-        provider = "azure";
-        break;
-      case "vke":
-        provider = "vultr";
-        break;
-      default:
-        provider = "";
-    }
-
-    // Check the server URL to see if we can detect the cluster provider.
-    // There's no standard URL format for GCP that's why it's not currently included
-    if (provider === "") {
-      const server = currentCluster.server;
-
-      if (server.includes("eks")) provider = "eks";
-      else if (server.includes("ondigitalocean")) provider = "digitalocean";
-      else if (server.includes("azmk8s")) provider = "azure";
-      else if (server.includes("vultr")) provider = "vultr";
-    }
-
-    // don't overwrite for templates that already have a source (i.e. non-Docker templates)
-    if (url && tag) {
-      _.set(values, "image.repository", url);
-      _.set(values, "image.tag", tag);
-    }
-
-    _.set(values, "ingress.provider", provider);
-
-    // pause jobs automatically
-    if (template?.metadata?.name == "job") {
-      _.set(values, "paused", true);
-    }
-
-    if (name === "") {
-      setSaveButtonStatus("App name cannot be empty");
-      return;
-    }
-    try {
-      await onSubmit(
-        {
-          name: name,
-          source_config_name: sourceConfig?.name || "",
-          template_name: templateInfo.name,
-          template_version: templateInfo.version,
-          values,
-        },
-        [...syncedEnvGroups]
-      );
-
-      setSaveButtonStatus("successful");
-      setTimeout(() => {
-        setSaveButtonStatus("");
-        setName("");
-        setTemplate(undefined);
-      }, 1000);
-    } catch (error) {
-      setSaveButtonStatus(error);
-      setTimeout(() => {
-        setSaveButtonStatus("");
-      }, 2000);
-    }
-  };
-
-  useEffect(() => {
-    let isSubscribed = true;
-    if (!templateInfo.name || !templateInfo.version) {
-      return () => {
-        isSubscribed = false;
-      };
-    }
-
-    setHasError(false);
-
-    api
-      .getTemplateInfo<ExpandedPorterTemplate>(
-        "<token>",
-        {},
-        {
-          project_id: currentProject.id,
-          name: templateInfo.name,
-          version: templateInfo.version,
-        }
-      )
-      .then((res) => {
-        if (isSubscribed) {
-          setTemplate(res.data);
-        }
-      })
-      .catch((err) => {
-        setHasError(true);
-      })
-      .finally(() => {
-        if (isSubscribed) {
-          setIsLoading(false);
-        }
-      });
-
-    return () => {
-      isSubscribed = false;
-    };
-  }, [templateInfo]);
-
-  if (isLoading) {
-    return (
-      <Wrapper>
-        <Loading />
-      </Wrapper>
-    );
-  }
-
-  if (hasError) {
-    return <>Unexpected error</>;
-  }
-  return (
-    <>
-      <TitleSection>
-        <BackButton onClick={onCancel}>
-          <i className="material-icons">keyboard_backspace</i>
-        </BackButton>
-        <Polymer>
-          <Icon src={hardcodedIcons[template.metadata.name]} />
-        </Polymer>
-        Add{" "}
-        {template.metadata.name.charAt(0).toUpperCase() +
-          template.metadata.name.slice(1)}{" "}
-        to Stack
-      </TitleSection>
-      <Heading>
-        Application Name <Required>*</Required>
-      </Heading>
-      <InputRow
-        type="string"
-        value={name}
-        setValue={(val: string) => setName(val)}
-        placeholder="ex: perspective-vortex"
-        width="470px"
-      />
-
-      <div style={{ position: "relative" }}>
-        <Heading>Application Settings</Heading>
-        <Helper>Configure settings for this application.</Helper>
-        <PorterFormWrapper
-          formData={template.form}
-          onSubmit={handleSubmit}
-          isLaunch
-          saveValuesStatus={saveButtonStatus}
-          saveButtonText="Add Application"
-          valuesToOverride={{ namespace }}
-          injectedProps={{
-            "key-value-array": {
-              availableSyncEnvGroups: availableEnvGroups.map(
-                parseEnvGroup(namespace)
-              ),
-            },
-          }}
-          includeMetadata
-        />
-      </div>
-    </>
-  );
-};
-
-export default NewAppResourceForm;
-
-const Required = styled.div`
-  margin-left: 8px;
-  color: #fc4976;
-  display: inline-block;
-`;
-
-const Wrapper = styled.div`
-  margin-top: calc(50vh - 150px);
-`;
-
-const StyledLaunchFlow = styled.div`
-  min-width: 300px;
-  width: calc(100% - 100px);
-  margin-left: 50px;
-  margin-top: ${(props: { disableMarginTop?: boolean }) =>
-    props.disableMarginTop ? "inherit" : "calc(50vh - 380px)"};
-  padding-bottom: 150px;
-`;

+ 0 - 165
dashboard/src/main/home/cluster-dashboard/stacks/components/NewEnvGroupForm.tsx

@@ -1,165 +0,0 @@
-import DynamicLink from "components/DynamicLink";
-import TitleSection from "components/TitleSection";
-import React, { useMemo, useState } from "react";
-import styled from "styled-components";
-import { BackButton, Polymer, SubmitButton } from "../launch/components/styles";
-import sliders from "assets/sliders.svg";
-import EnvGroupArray, { KeyValueType } from "../../env-groups/EnvGroupArray";
-import Heading from "components/form-components/Heading";
-import { isAlphanumeric } from "shared/common";
-import InputRow from "components/form-components/InputRow";
-import Helper from "components/form-components/Helper";
-
-const envArrayToObject = (variables: KeyValueType[]) => {
-  return variables.reduce<{ [key: string]: string }>((acc, curr) => {
-    acc[curr.key] = curr.value;
-    return acc;
-  }, {});
-};
-
-type ProcessedEnvVariables = ReturnType<typeof envArrayToObject>;
-
-const NewEnvGroupForm = (props: {
-  onSubmit: (newEnvGroup: {
-    name: string;
-    variables: ProcessedEnvVariables;
-    secret_variables: ProcessedEnvVariables;
-  }) => Promise<void>;
-  onCancel: () => void;
-}) => {
-  const { onSubmit, onCancel } = props;
-
-  const [name, setName] = useState("");
-  const [envVariables, setEnvVariables] = useState<KeyValueType[]>([]);
-  const [submitError, setSubmitError] = useState("");
-
-  const handleOnSubmit = async () => {
-    const variables = envVariables.filter(
-      (variable) => !variable.locked && !variable.hidden
-    );
-    const secret_variables = envVariables.filter(
-      (variable) => variable.locked || variable.hidden
-    );
-
-    try {
-      await onSubmit({
-        name: name,
-        variables: envArrayToObject(variables),
-        secret_variables: envArrayToObject(secret_variables),
-      });
-    } catch (error) {
-      setSubmitError(error);
-      return;
-    }
-
-    setName("");
-    setEnvVariables([]);
-    return;
-  };
-
-  const hasError = useMemo(() => {
-    if (!isAlphanumeric(name) || name === "") {
-      return { message: "Name cannot be empty." };
-    }
-
-    if (!envVariables.length) {
-      return { message: "Please add at least one environment variable." };
-    }
-
-    if (envVariables.some((variable) => !variable.value || !variable.key)) {
-      return { message: "Please fill in all environment variables." };
-    }
-
-    return null;
-  }, [name, envVariables]);
-
-  return (
-    <>
-      <TitleSection>
-        <BackButton onClick={onCancel}>
-          <i className="material-icons">keyboard_backspace</i>
-        </BackButton>
-        <Polymer>
-          <SliderIcon src={sliders} />
-        </Polymer>
-        Add a Env Group to Stack
-      </TitleSection>
-      <Heading isAtTop={true}>Name</Heading>
-      <Subtitle>
-        <Warning
-          makeFlush={true}
-          highlight={!isAlphanumeric(name) && name !== ""}
-        >
-          Lowercase letters, numbers, and "-" only.
-        </Warning>
-      </Subtitle>
-      <InputRow
-        type="text"
-        value={name}
-        setValue={(x: string) => {
-          setName(x);
-        }}
-        placeholder="ex: my-env-group"
-        width="100%"
-      />
-
-      <Heading>Environment variables</Heading>
-      <Helper>
-        Set environment variables for your secrets and environment-specific
-        configuration.
-      </Helper>
-      <EnvGroupArray
-        values={envVariables}
-        setValues={(x: any) => setEnvVariables((prev) => [...x])}
-        fileUpload={true}
-        secretOption={true}
-      />
-
-      <SubmitButton
-        onClick={handleOnSubmit}
-        makeFlush
-        clearPosition
-        text="Save env group"
-        disabled={!!hasError}
-        statusPosition="left"
-        status={hasError?.message || submitError || ""}
-      />
-    </>
-  );
-};
-
-export default NewEnvGroupForm;
-
-const SliderIcon = styled.img`
-  width: 25px;
-  margin-right: 16px;
-
-  opacity: 0;
-  animation: floatIn 0.5s 0.2s;
-  animation-fill-mode: forwards;
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(10px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-
-const Subtitle = styled.div`
-  padding: 11px 0px 0px;
-  font-family: "Work Sans", sans-serif;
-  font-size: 13px;
-  color: #aaaabb;
-  line-height: 1.6em;
-  display: flex;
-  align-items: center;
-`;
-
-const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>`
-  color: ${(props) => (props.highlight ? "#f5cb42" : "")};
-  margin-left: ${(props) => (props.makeFlush ? "" : "5px")};
-`;

+ 0 - 25
dashboard/src/main/home/cluster-dashboard/stacks/components/Status.tsx

@@ -1,25 +0,0 @@
-import React from "react";
-import { StatusStyles } from "./styles";
-import loading from "assets/loading.gif";
-
-export type StatusProps = {
-  status: "loading" | "failed" | "successful" | "unknown";
-  message: string;
-  className?: string;
-};
-
-const Status = ({ status, message, className }: StatusProps) => {
-  return (
-    <>
-      <StatusStyles.Status className={className}>
-        {status === "loading" && <StatusStyles.Spinner src={loading} />}
-        {status === "failed" && <StatusStyles.Failed />}
-        {status === "successful" && <StatusStyles.Successful />}
-        {status === "unknown" && <StatusStyles.Unknown />}
-        {message}
-      </StatusStyles.Status>
-    </>
-  );
-};
-
-export default Status;

+ 0 - 67
dashboard/src/main/home/cluster-dashboard/stacks/launch/NewApp.tsx

@@ -1,67 +0,0 @@
-import _ from "lodash";
-import React, { useContext } from "react";
-import { useParams } from "react-router";
-import { useRouting } from "shared/routing";
-import { StacksLaunchContext } from "./Store";
-import styled from "styled-components";
-import NewAppResourceForm from "../components/NewAppResourceForm";
-
-const DEFAULT_STACK_SOURCE_CONFIG_INDEX = 0;
-
-const NewApp = () => {
-  const { addAppResource, newStack, namespace } = useContext(
-    StacksLaunchContext
-  );
-
-  const params = useParams<{
-    template_name: string;
-    version: string;
-  }>();
-
-  const { pushFiltered } = useRouting();
-
-  return (
-    <>
-      <NewAppResourceForm
-        sourceConfig={
-          newStack.source_configs[DEFAULT_STACK_SOURCE_CONFIG_INDEX]
-        }
-        availableEnvGroups={newStack.env_groups}
-        namespace={namespace}
-        templateInfo={{
-          name: params.template_name,
-          version: params.version,
-        }}
-        onSubmit={async (newApp, syncedEnvGroups) => {
-          addAppResource(newApp, syncedEnvGroups);
-          pushFiltered("/stacks/launch/overview", []);
-          return;
-        }}
-        onCancel={() => {
-          pushFiltered("/stacks/launch/overview", []);
-        }}
-      />
-    </>
-  );
-};
-
-export default NewApp;
-
-const Required = styled.div`
-  margin-left: 8px;
-  color: #fc4976;
-  display: inline-block;
-`;
-
-const Wrapper = styled.div`
-  margin-top: calc(50vh - 150px);
-`;
-
-const StyledLaunchFlow = styled.div`
-  min-width: 300px;
-  width: calc(100% - 100px);
-  margin-left: 50px;
-  margin-top: ${(props: { disableMarginTop?: boolean }) =>
-    props.disableMarginTop ? "inherit" : "calc(50vh - 380px)"};
-  padding-bottom: 150px;
-`;

+ 0 - 30
dashboard/src/main/home/cluster-dashboard/stacks/launch/NewEnvGroup.tsx

@@ -1,30 +0,0 @@
-import React, { useContext } from "react";
-import { useRouting } from "shared/routing";
-import styled from "styled-components";
-import NewEnvGroupForm from "../components/NewEnvGroupForm";
-import { StacksLaunchContext } from "./Store";
-
-const NewEnvGroup = () => {
-  const { addEnvGroup } = useContext(StacksLaunchContext);
-
-  const { pushFiltered } = useRouting();
-
-  return (
-    <NewEnvGroupForm
-      onSubmit={async (newEnvGroup) => {
-        addEnvGroup({
-          ...newEnvGroup,
-          linked_applications: [],
-        });
-        pushFiltered("/stacks/launch/overview", []);
-        return;
-      }}
-      onCancel={() => {
-        pushFiltered("/stacks/launch/overview", []);
-        return;
-      }}
-    />
-  );
-};
-
-export default NewEnvGroup;

+ 0 - 315
dashboard/src/main/home/cluster-dashboard/stacks/launch/Overview.tsx

@@ -1,315 +0,0 @@
-import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
-import { StacksLaunchContext } from "./Store";
-import InputRow from "components/form-components/InputRow";
-import Selector from "components/Selector";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import useAuth from "shared/auth/useAuth";
-import { useRouting } from "shared/routing";
-import {
-  AddResourceButtonStyles,
-  SubmitButton,
-  Card,
-} from "./components/styles";
-import { AddResourceButton } from "./components/AddResourceButton";
-import styled from "styled-components";
-
-import Helper from "components/form-components/Helper";
-import Heading from "components/form-components/Heading";
-import TitleSection from "components/TitleSection";
-import DynamicLink from "components/DynamicLink";
-import { hardcodedIcons } from "shared/hardcodedNameDict";
-import sliders from "assets/sliders.svg";
-import DocsHelper from "components/DocsHelper";
-
-const Overview = () => {
-  const {
-    newStack,
-    namespace,
-    setStackName,
-    setStackNamespace,
-    submit,
-    removeAppResource,
-    removeEnvGroup,
-  } = useContext(StacksLaunchContext);
-  const { currentProject, currentCluster } = useContext(Context);
-  const [isAuthorized] = useAuth();
-
-  const [namespaceOptions, setNamespaceOptions] = useState<
-    { label: string; value: string }[]
-  >([]);
-
-  const [submitButtonStatus, setSubmitButtonStatus] = useState("");
-
-  const { pushFiltered } = useRouting();
-
-  const updateNamespaces = (cluster_id: number) => {
-    api
-      .getNamespaces(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          cluster_id,
-        }
-      )
-      .then((res) => {
-        if (res.data) {
-          const availableNamespaces = res.data.filter((namespace: any) => {
-            return namespace.status !== "Terminating";
-          });
-          const namespaceOptions = availableNamespaces.map(
-            (x: { name: string }) => {
-              return { label: x.name, value: x.name };
-            }
-          );
-          if (availableNamespaces.length > 0) {
-            setNamespaceOptions(namespaceOptions);
-          }
-        }
-      })
-      .catch(console.log);
-  };
-
-  const handleSubmit = () => {
-    setSubmitButtonStatus("loading");
-
-    submit().then(() => {
-      console.log("submit");
-      setTimeout(() => {
-        setSubmitButtonStatus("");
-        pushFiltered("/stacks", []);
-      }, 1000);
-    });
-  };
-
-  useEffect(() => {
-    updateNamespaces(currentCluster.id);
-  }, [currentCluster]);
-
-  const isValid = useMemo(() => {
-    if (namespace === "") {
-      return false;
-    }
-
-    if (newStack.name === "") {
-      return false;
-    }
-
-    if (newStack.source_configs.length === 0) {
-      return false;
-    }
-
-    if (newStack.app_resources.length === 0) {
-      return false;
-    }
-
-    return true;
-  }, [namespace, newStack.name]);
-
-  return (
-    <>
-      <TitleSection handleNavBack={() => window.open("/stacks", "_self")}>
-        <Polymer>
-          <i className="material-icons">lan</i>
-        </Polymer>
-        New Application Stack
-      </TitleSection>
-
-      <Heading>Stack Name</Heading>
-      <Helper>
-        Give this application stack a unique name:
-        <Required>*</Required>
-      </Helper>
-      <InputRow
-        type="string"
-        placeholder="ex: perspective-vortices"
-        width="470px"
-        value={newStack.name}
-        setValue={(newName: string) => setStackName(newName)}
-      />
-
-      <Heading>Destination</Heading>
-      <Helper>
-        Specify the namespace you would like to deploy this stack to.
-      </Helper>
-      <ClusterSection>
-        <NamespaceLabel>
-          <i className="material-icons">view_list</i> Namespace
-        </NamespaceLabel>
-        <Selector
-          key={"namespace"}
-          refreshOptions={() => {
-            updateNamespaces(currentCluster.id);
-          }}
-          addButton={isAuthorized("namespace", "", ["get", "create"])}
-          activeValue={namespace}
-          setActiveValue={(val) => setStackNamespace(val)}
-          options={namespaceOptions}
-          width="250px"
-          dropdownWidth="335px"
-          closeOverlay={true}
-        />
-      </ClusterSection>
-
-      <Heading>
-        Env Groups
-        {/* <InlineDocsHelper
-          disableMargin={true}
-          tooltipText="Environment groups"
-          link="https://docs.porter.run/deploying-applications/environment-groups"
-        /> */}
-      </Heading>
-      <Helper>Add scoped environment groups to this stack:</Helper>
-      <Card.Grid>
-        {newStack.env_groups.map((envGroup) => (
-          <Card.Wrapper variant="unclickable">
-            <Card.Title>
-              <Card.SmallerIcon src={sliders} />
-              {envGroup.name}
-            </Card.Title>
-            <Card.Actions>
-              <Card.ActionButton
-                onClick={() => {
-                  removeEnvGroup(envGroup);
-                }}
-              >
-                <i className="material-icons-outlined">close</i>
-              </Card.ActionButton>
-            </Card.Actions>
-          </Card.Wrapper>
-        ))}
-
-        <AddResourceButtonStyles.Wrapper>
-          <AddResourceButtonStyles.Flex>
-            <LinkMask to={`/stacks/launch/new-env-group`}></LinkMask>
-            <Icon>
-              <i className="material-icons">add</i>
-            </Icon>
-            Add a new env group
-          </AddResourceButtonStyles.Flex>
-        </AddResourceButtonStyles.Wrapper>
-      </Card.Grid>
-
-      <Heading>Applications</Heading>
-      <Helper>
-        At least one application is required:
-        <Required>*</Required>
-      </Helper>
-      <Card.Grid>
-        {newStack.app_resources.map((app) => (
-          <Card.Wrapper variant="unclickable">
-            <Card.Title>
-              <Card.Icon src={hardcodedIcons[app.template_name]}></Card.Icon>
-              {app.name}
-            </Card.Title>
-            <Card.Actions>
-              <Card.ActionButton
-                onClick={() => {
-                  removeAppResource(app);
-                }}
-              >
-                <i className="material-icons-outlined">close</i>
-              </Card.ActionButton>
-            </Card.Actions>
-          </Card.Wrapper>
-        ))}
-
-        <AddResourceButton />
-      </Card.Grid>
-
-      <SubmitButton
-        disabled={!isValid || submitButtonStatus !== ""}
-        text="Create stack"
-        onClick={handleSubmit}
-        clearPosition
-        statusPosition="left"
-        status={submitButtonStatus}
-      >
-        Create stack
-      </SubmitButton>
-    </>
-  );
-};
-
-export default Overview;
-
-const NamespaceLabel = styled.div`
-  margin-right: 10px;
-  display: flex;
-  align-items: center;
-  > i {
-    font-size: 16px;
-    margin-right: 6px;
-  }
-`;
-
-const ClusterSection = styled.div`
-  display: flex;
-  align-items: center;
-  color: #ffffff;
-  font-family: "Work Sans", sans-serif;
-  font-size: 14px;
-  margin-top: 20px;
-  font-weight: 500;
-  margin-bottom: 32px;
-
-  > i {
-    font-size: 25px;
-    color: #ffffff44;
-    margin-right: 13px;
-  }
-`;
-
-const Br = styled.div<{ height?: string }>`
-  width: 100%;
-  height: ${(props) => props.height || "1px"};
-`;
-
-const Required = styled.div`
-  margin-left: 8px;
-  color: #fc4976;
-  display: inline-block;
-`;
-
-const Polymer = styled.div`
-  margin-bottom: -6px;
-
-  > i {
-    color: #ffffff;
-    font-size: 24px;
-    margin-left: 5px;
-    margin-right: 18px;
-  }
-`;
-
-const StyledLaunchFlow = styled.div`
-  min-width: 300px;
-  width: calc(100% - 100px);
-  margin-left: 50px;
-  margin-top: ${(props: { disableMarginTop?: boolean }) =>
-    props.disableMarginTop ? "inherit" : "calc(50vh - 380px)"};
-  padding-bottom: 150px;
-`;
-
-const LinkMask = styled(DynamicLink)`
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-`;
-
-const Icon = styled.div`
-  margin-bottom: -3px;
-  > i {
-    margin-right: 20px;
-    margin-left: 9px;
-    font-size: 20px;
-    color: #aaaabb;
-  }
-`;
-
-const InlineDocsHelper = styled(DocsHelper)`
-  display: inline-block;
-`;

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott