From 989cf9bb487c321f552ed9c6cb52f3d082327ee3 Mon Sep 17 00:00:00 2001
From: CristhianF7 <CristhianF7@gmail.com>
Date: Wed, 17 May 2023 18:41:55 -0500
Subject: [PATCH 1/6] feat: marketplace

---
 components/card/card.styled.ts                |   2 +-
 components/checkbox/index.tsx                 |   2 +-
 .../gitProviderButton.styled.ts               |   2 +-
 .../installationStepContainer/index.tsx       |   4 +-
 components/marketplaceCard/index.tsx          |  50 +++++++
 .../marketplaceCard/marketplaceCard.styled.ts |  32 +++++
 components/marketplaceModal/index.tsx         |  76 +++++++++++
 .../marketplaceModal.styled.ts                |  32 +++++
 components/modal/index.tsx                    |   7 +-
 components/password/index.tsx                 |  21 +--
 components/service/index.tsx                  |   6 +
 components/service/service.styled.ts          |   2 +-
 components/tab/index.tsx                      |   4 +-
 components/textField/index.tsx                |   4 +-
 containers/clusterManagement/index.tsx        |   4 +-
 containers/conciseLogs/index.tsx              |  10 +-
 containers/marketplace/index.tsx              | 128 ++++++++++++++++++
 containers/marketplace/marketplace.styled.ts  |  31 +++++
 containers/provision/index.tsx                |   6 +-
 containers/services/index.tsx                 |  95 ++++++++++---
 containers/services/services.styled.ts        |   5 +-
 containers/terminalLogs/terminalLogs.tsx      |  10 +-
 declaration.d.ts                              |   1 +
 next.config.js                                |   6 +
 pages/services.tsx                            |   3 +
 redux/api/index.ts                            |   1 +
 .../slices/{cluster.slice.ts => api.slice.ts} |  27 +++-
 redux/slices/index.ts                         |   2 +-
 redux/store.ts                                |   4 +-
 .../thunks/{cluster.thunk.ts => api.thunk.ts} |  34 ++++-
 theme/index.ts                                |   2 +
 types/marketplace/index.ts                    |   7 +
 32 files changed, 534 insertions(+), 86 deletions(-)
 create mode 100644 components/marketplaceCard/index.tsx
 create mode 100644 components/marketplaceCard/marketplaceCard.styled.ts
 create mode 100644 components/marketplaceModal/index.tsx
 create mode 100644 components/marketplaceModal/marketplaceModal.styled.ts
 create mode 100644 containers/marketplace/index.tsx
 create mode 100644 containers/marketplace/marketplace.styled.ts
 rename redux/slices/{cluster.slice.ts => api.slice.ts} (82%)
 rename redux/thunks/{cluster.thunk.ts => api.thunk.ts} (81%)
 create mode 100644 types/marketplace/index.ts

diff --git a/components/card/card.styled.ts b/components/card/card.styled.ts
index 0e2968a4..039d6ccd 100644
--- a/components/card/card.styled.ts
+++ b/components/card/card.styled.ts
@@ -3,7 +3,7 @@ import styled, { css } from 'styled-components';
 import { CardProps } from '.';
 
 export const CardContainer = styled.div<CardProps>`
-  border: 2px solid #e2e8f0;
+  border: 2px solid ${({ theme }) => theme.colors.pastelLightBlue};
   border-radius: 8px;
   background-color: white;
   cursor: pointer;
diff --git a/components/checkbox/index.tsx b/components/checkbox/index.tsx
index 6a45baae..787362d3 100644
--- a/components/checkbox/index.tsx
+++ b/components/checkbox/index.tsx
@@ -10,7 +10,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(p
     <CheckboxMUI
       {...props}
       inputRef={ref}
-      style={{ border: '1px' }}
+      style={{ border: '1px', padding: 0 }}
       icon={
         <CheckBoxOutlineBlankIcon
           sx={{ borderRadius: '4px', color: props.disabled ? `${QUARTZ}` : `${SPUN_PEARL}` }}
diff --git a/components/gitProviderButton/gitProviderButton.styled.ts b/components/gitProviderButton/gitProviderButton.styled.ts
index 7b89f62a..43fef41c 100644
--- a/components/gitProviderButton/gitProviderButton.styled.ts
+++ b/components/gitProviderButton/gitProviderButton.styled.ts
@@ -6,7 +6,7 @@ export const Button = styled.button<{ active?: boolean }>`
   align-items: center;
   width: 260px;
   height: 84px;
-  border: 2px solid #e2e8f0;
+  border: 2px solid ${({ theme }) => theme.colors.pastelLightBlue};
   border-radius: 8px;
   background-color: white;
   cursor: pointer;
diff --git a/components/installationStepContainer/index.tsx b/components/installationStepContainer/index.tsx
index fc375e8d..e46274a1 100644
--- a/components/installationStepContainer/index.tsx
+++ b/components/installationStepContainer/index.tsx
@@ -37,8 +37,8 @@ const InstallationStepContainer: FunctionComponent<InstallationStepContainerProp
   children,
   ...rest
 }) => {
-  const { completedSteps, isProvisioned } = useAppSelector(({ cluster }) => ({
-    ...cluster,
+  const { completedSteps, isProvisioned } = useAppSelector(({ api }) => ({
+    ...api,
   }));
   const progress = useMemo(() => {
     const clusterChecks = Object.keys(CLUSTER_CHECKS);
diff --git a/components/marketplaceCard/index.tsx b/components/marketplaceCard/index.tsx
new file mode 100644
index 00000000..634ac76a
--- /dev/null
+++ b/components/marketplaceCard/index.tsx
@@ -0,0 +1,50 @@
+import React, { FunctionComponent, PropsWithChildren } from 'react';
+import Image from 'next/image';
+
+import Tag from '../tag';
+import { MarketplaceApp } from '../../types/marketplace';
+import Button from '../../components/button';
+import Typography from '../typography';
+import { VOLCANIC_SAND } from '../../constants/colors';
+
+import { Card, Description, Header } from './marketplaceCard.styled';
+
+export interface MarketplaceCardProps extends MarketplaceApp {
+  onClick?: () => void;
+  showSubmitButton?: boolean;
+}
+
+const MarketplaceCard: FunctionComponent<PropsWithChildren<MarketplaceCardProps>> = ({
+  name,
+  categories,
+  image_url,
+  description,
+  onClick,
+  showSubmitButton = true,
+  children,
+}) => {
+  return (
+    <Card>
+      <Header>
+        <Image alt={name} height={28} width={28} src={image_url} style={{ objectFit: 'contain' }} />
+        <Typography
+          variant="buttonSmall"
+          sx={{ textTransform: 'capitalize' }}
+          color={VOLCANIC_SAND}
+        >
+          {name}
+        </Typography>
+        {categories &&
+          categories.map((category) => <Tag key={category} text={category} bgColor="purple" />)}
+      </Header>
+      <Description variant="body2">{description || children}</Description>
+      {showSubmitButton && (
+        <Button variant="outlined" color="secondary" onClick={onClick}>
+          Add
+        </Button>
+      )}
+    </Card>
+  );
+};
+
+export default MarketplaceCard;
diff --git a/components/marketplaceCard/marketplaceCard.styled.ts b/components/marketplaceCard/marketplaceCard.styled.ts
new file mode 100644
index 00000000..7d24ab6f
--- /dev/null
+++ b/components/marketplaceCard/marketplaceCard.styled.ts
@@ -0,0 +1,32 @@
+import styled from 'styled-components';
+
+import Typography from '../../components/typography';
+
+export const Card = styled.div`
+  background-color: ${({ theme }) => theme.colors.white};
+  border: 1px solid ${({ theme }) => theme.colors.pastelLightBlue};
+  border-radius: 12px;
+  height: 194px;
+  padding: 24px;
+  width: 372px;
+`;
+
+export const Description = styled(Typography)`
+  margin: 16px 0;
+  color: ${({ theme }) => theme.colors.saltboxBlue};
+
+  & a {
+    color: ${({ theme }) => theme.colors.primary};
+    text-decoration: none;
+  }
+
+  & a:hover {
+    text-decoration: underline;
+  }
+`;
+
+export const Header = styled.div`
+  align-items: center;
+  display: flex;
+  gap: 16px;
+`;
diff --git a/components/marketplaceModal/index.tsx b/components/marketplaceModal/index.tsx
new file mode 100644
index 00000000..fff0fe1e
--- /dev/null
+++ b/components/marketplaceModal/index.tsx
@@ -0,0 +1,76 @@
+import React, { FunctionComponent } from 'react';
+import { Box, Divider } from '@mui/material';
+import Image from 'next/image';
+import { useForm } from 'react-hook-form';
+
+import Modal from '../modal';
+import Button from '../button';
+import Typography from '../typography';
+import ControlledPassword from '../controlledFields/Password';
+import { MarketplaceApp } from '../../types/marketplace';
+import { BISCAY, SALTBOX_BLUE } from '../../constants/colors';
+
+import { Content, Close, Footer, Header } from './marketplaceModal.styled';
+
+export interface MarketplaceModalProps extends MarketplaceApp {
+  isOpen: boolean;
+  closeModal: () => void;
+}
+
+const MarketplaceModal: FunctionComponent<MarketplaceModalProps> = ({
+  closeModal,
+  isOpen,
+  name,
+  image_url,
+  secret_keys,
+}) => {
+  const { control } = useForm();
+  return (
+    <Modal isOpen={isOpen} padding={0}>
+      <Box
+        sx={{
+          width: '630px',
+          height: 'auto',
+          backgroundColor: 'white',
+          borderRadius: '8px',
+          boxShadow: '0px 2px 4px rgba(100, 116, 139, 0.1)',
+        }}
+      >
+        <Header>
+          <Image alt={name} src={image_url} width={30} height={30} />
+          <Typography variant="h6" color={BISCAY}>
+            {name}
+          </Typography>
+          <Close onClick={closeModal} htmlColor={SALTBOX_BLUE} fontSize="medium" />
+        </Header>
+        <Divider />
+        <Content>
+          {secret_keys &&
+            secret_keys.map((key) => (
+              <ControlledPassword
+                key={key}
+                control={control}
+                name={key}
+                label={key}
+                rules={{
+                  required: true,
+                }}
+                required
+              />
+            ))}
+        </Content>
+        <Divider />
+        <Footer>
+          <Button variant="text" color="info" onClick={closeModal}>
+            Cancel
+          </Button>
+          <Button variant="contained" color="primary">
+            Add
+          </Button>
+        </Footer>
+      </Box>
+    </Modal>
+  );
+};
+
+export default MarketplaceModal;
diff --git a/components/marketplaceModal/marketplaceModal.styled.ts b/components/marketplaceModal/marketplaceModal.styled.ts
new file mode 100644
index 00000000..05686b5b
--- /dev/null
+++ b/components/marketplaceModal/marketplaceModal.styled.ts
@@ -0,0 +1,32 @@
+import styled from 'styled-components';
+import CloseIcon from '@mui/icons-material/Close';
+
+export const Content = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+  padding: 32px 24px;
+`;
+
+export const Close = styled(CloseIcon)`
+  cursor: pointer;
+  position: fixed;
+  top: 25px;
+  right: 25px;
+`;
+
+export const Footer = styled.div`
+  display: flex;
+  gap: 16px;
+  justify-content: flex-end;
+  padding: 16px 24px;
+  width: calc(100% - 48px);
+`;
+
+export const Header = styled.div`
+  display: flex;
+  gap: 16px;
+  padding: 24px;
+  position: relative;
+  text-transform: capitalize;
+`;
diff --git a/components/modal/index.tsx b/components/modal/index.tsx
index 63e23fd5..09f9292a 100644
--- a/components/modal/index.tsx
+++ b/components/modal/index.tsx
@@ -9,7 +9,6 @@ const style = {
   transform: 'translate(-50%, -50%)',
   width: 'auto',
   borderRadius: '8px',
-  p: 4,
   zIndex: 2000,
 };
 
@@ -19,6 +18,7 @@ export interface IModalProps {
   children: React.ReactElement;
   isOpen: boolean;
   onCloseModal?: () => void;
+  padding?: number;
 }
 
 const Modal: FunctionComponent<IModalProps> = ({
@@ -27,6 +27,7 @@ const Modal: FunctionComponent<IModalProps> = ({
   children,
   isOpen,
   onCloseModal,
+  padding = 4,
 }) => (
   <ModalMui
     open={isOpen}
@@ -35,7 +36,9 @@ const Modal: FunctionComponent<IModalProps> = ({
     aria-describedby="modal-description"
     sx={{ zIndex: 2000 }}
   >
-    <Box sx={{ ...style, backgroundColor, boxShadow: boxShadow && 24 }}>{children}</Box>
+    <Box sx={{ ...style, p: padding, backgroundColor, boxShadow: boxShadow ? 24 : 0 }}>
+      {children}
+    </Box>
   </ModalMui>
 );
 
diff --git a/components/password/index.tsx b/components/password/index.tsx
index c4cb1a35..5be4ad48 100644
--- a/components/password/index.tsx
+++ b/components/password/index.tsx
@@ -1,31 +1,12 @@
 import React, { FunctionComponent, MouseEvent, useState } from 'react';
 import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
 import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
-import { IconButton, InputBase, InputProps, styled } from '@mui/material';
+import { IconButton, InputProps } from '@mui/material';
 
 import TextField from '../textField';
 
 import { InputAdornmentContainer } from './password.styled';
 
-export const Input = styled(InputBase)(({ theme }) => ({
-  '& .MuiInputBase-input': {
-    'borderRadius': 4,
-    'border': '1px solid #ced4da',
-    'fontSize': 14,
-    'height': 18,
-    'lineHeight': 20,
-    'letterSpacing': 0.25,
-    'padding': '8px 40px 8px 12px',
-    'position': 'relative',
-    '&:focus': {
-      border: `1px solid ${theme.palette.primary.main}`,
-    },
-  },
-  '& .MuiInputBase-adornedEnd': {
-    'margin-bottom': '10px',
-  },
-}));
-
 export interface PasswordProps extends InputProps {
   label: string;
   helperText?: string;
diff --git a/components/service/index.tsx b/components/service/index.tsx
index 29b3ed70..6a906799 100644
--- a/components/service/index.tsx
+++ b/components/service/index.tsx
@@ -120,6 +120,12 @@ const Service: FunctionComponent<ServiceProps> = ({
       <Header>
         <Image src={serviceLogo} alt={name} width="24" />
         <Title variant="subtitle2">{name}</Title>
+        {/* <NextImage
+          src={`https://argocd.mgmt-20.kubefirst.com/api/badge?name=${name.toLowerCase()}`}
+          width={120}
+          height={20}
+          alt={name}
+        /> */}
       </Header>
       <Description variant="body2">{description}</Description>
       {links && !children ? linksComponent : children}
diff --git a/components/service/service.styled.ts b/components/service/service.styled.ts
index a1ad2ba1..489dcf22 100644
--- a/components/service/service.styled.ts
+++ b/components/service/service.styled.ts
@@ -8,7 +8,7 @@ import { PASTEL_LIGHT_BLUE } from '../../constants/colors';
 
 export const AppConnector = styled.div`
   height: 16px;
-  background-color: #e2e8f0;
+  background-color: ${({ theme }) => theme.colors.pastelLightBlue};
   top: 8px;
   left: 3px;
   position: absolute;
diff --git a/components/tab/index.tsx b/components/tab/index.tsx
index 663eac15..bc76988a 100644
--- a/components/tab/index.tsx
+++ b/components/tab/index.tsx
@@ -1,4 +1,4 @@
-import React, { FunctionComponent } from 'react';
+import React, { FunctionComponent, ReactNode } from 'react';
 import { styled, Tab as MuiTab, tabClasses } from '@mui/material';
 
 import { ECHO_BLUE } from '../../constants/colors';
@@ -19,7 +19,7 @@ export const a11yProps = (index: number) => {
   };
 };
 
-export const Tab = styled((props: { color?: string; label: string }) => (
+export const Tab = styled((props: { color?: string; label: string | ReactNode }) => (
   <MuiTab disableRipple {...props} />
 ))(({ theme, color }) => ({
   ...theme.typography.labelMedium,
diff --git a/components/textField/index.tsx b/components/textField/index.tsx
index 14a64493..31ef3ed8 100644
--- a/components/textField/index.tsx
+++ b/components/textField/index.tsx
@@ -11,7 +11,7 @@ export interface TextFieldProps extends InputProps {
   helperText?: string;
 }
 
-export const Input = styled(InputBase)(({ theme, error }) => ({
+export const Input = styled(InputBase)(({ theme, error, type, endAdornment }) => ({
   '& .MuiInputBase-input': {
     'borderRadius': 4,
     'border': `1px solid ${error ? theme.palette.error.main : '#ced4da'}`,
@@ -19,7 +19,7 @@ export const Input = styled(InputBase)(({ theme, error }) => ({
     'height': 18,
     'lineHeight': 20,
     'letterSpacing': 0.25,
-    'padding': '8px 12px',
+    'padding': type === 'password' || !!endAdornment ? '8px 40px 8px 12px' : '8px 12px',
     'width': '100%',
     '&:focus': {
       border: `1px solid ${error ? theme.palette.error.main : theme.palette.primary.main}`,
diff --git a/containers/clusterManagement/index.tsx b/containers/clusterManagement/index.tsx
index d18223d0..c8777228 100644
--- a/containers/clusterManagement/index.tsx
+++ b/containers/clusterManagement/index.tsx
@@ -8,7 +8,7 @@ import Typography from '../../components/typography';
 import Table from '../../components/table';
 import { DELETE_OPTION, VIEW_DETAILS_OPTION } from '../../constants/cluster';
 import { useAppDispatch, useAppSelector } from '../../redux/store';
-import { deleteCluster, getCluster, getClusters } from '../../redux/thunks/cluster.thunk';
+import { deleteCluster, getCluster, getClusters } from '../../redux/thunks/api.thunk';
 import { resetInstallState } from '../../redux/slices/installation.slice';
 import { setConfigValues } from '../../redux/slices/config.slice';
 import { Cluster, ClusterRequestProps } from '../../types/provision';
@@ -43,7 +43,7 @@ const ClusterManagement: FunctionComponent<ClusterManagementProps> = ({ apiUrl,
   const { push } = useRouter();
 
   const dispatch = useAppDispatch();
-  const { isDeleted, isDeleting, isError, clusters } = useAppSelector(({ cluster }) => cluster);
+  const { isDeleted, isDeleting, isError, clusters } = useAppSelector(({ api }) => api);
 
   const handleMenuClick = (option: string, rowItem: Row) => {
     const { clusterName } = rowItem;
diff --git a/containers/conciseLogs/index.tsx b/containers/conciseLogs/index.tsx
index 623f8c79..3641cbe2 100644
--- a/containers/conciseLogs/index.tsx
+++ b/containers/conciseLogs/index.tsx
@@ -28,11 +28,11 @@ export interface ConciseLogsProps {
 
 const ConciseLogs: FunctionComponent<ConciseLogsProps> = ({ completedSteps }) => {
   const { installType, isError, isProvisioned, lastErrorCondition } = useAppSelector(
-    ({ cluster, installation }) => ({
-      cluster: cluster.selectedCluster,
-      isProvisioned: cluster.isProvisioned,
-      lastErrorCondition: cluster.lastErrorCondition,
-      isError: cluster.isError,
+    ({ api, installation }) => ({
+      cluster: api.selectedCluster,
+      isProvisioned: api.isProvisioned,
+      lastErrorCondition: api.lastErrorCondition,
+      isError: api.isError,
       installType: installation.installType,
     }),
   );
diff --git a/containers/marketplace/index.tsx b/containers/marketplace/index.tsx
new file mode 100644
index 00000000..24901f6a
--- /dev/null
+++ b/containers/marketplace/index.tsx
@@ -0,0 +1,128 @@
+import React, { FunctionComponent, useMemo, useState } from 'react';
+import { FormControlLabel, FormGroup } from '@mui/material';
+import intersection from 'lodash/intersection';
+import NextLink from 'next/link';
+
+import Checkbox from '../../components/checkbox';
+import Typography from '../../components/typography';
+import MarketplaceCard from '../../components/marketplaceCard';
+import MarketplaceModal from '../../components/marketplaceModal';
+import useModal from '../../hooks/useModal';
+import { useAppSelector } from '../../redux/store';
+import { MarketplaceApp } from '../../types/marketplace';
+import { VOLCANIC_SAND } from '../../constants/colors';
+
+import { CardsContainer, Container, Content, Filter } from './marketplace.styled';
+
+const STATIC_HELP_CARD: MarketplaceApp = {
+  categories: [],
+  name: 'Can’t find what you need?',
+  image_url:
+    'https://raw.githubusercontent.com/kubefirst/kubefirst/main/images/kubefirst-light.svg',
+};
+
+const Marketplace: FunctionComponent = () => {
+  const [selectedCategories, setSelectedCategories] = useState<Array<string>>([]);
+  const [selectedApp, setSelectedApp] = useState<MarketplaceApp>();
+
+  const { isOpen, openModal, closeModal } = useModal();
+
+  const marketplaceApps = useAppSelector(({ api }) => api.marketplaceApps);
+  const categories = useMemo(
+    () =>
+      marketplaceApps
+        .map(({ categories }) => categories)
+        .reduce((previous, current) => {
+          const values = current.filter((category) => !previous.includes(category));
+          return [...previous, ...values];
+        }, []),
+    [marketplaceApps],
+  );
+
+  const onClickCategory = (category: string) => {
+    const isCategorySelected = selectedCategories.includes(category);
+
+    if (isCategorySelected) {
+      setSelectedCategories(
+        selectedCategories.filter((selectedCategory) => selectedCategory !== category),
+      );
+    } else {
+      setSelectedCategories([...selectedCategories, category]);
+    }
+  };
+
+  const handleSelectedApp = (app: MarketplaceApp) => {
+    setSelectedApp(app);
+    openModal();
+  };
+
+  const filteredApps = useMemo(() => {
+    if (!selectedCategories.length) {
+      return marketplaceApps;
+    }
+
+    return (
+      marketplaceApps &&
+      marketplaceApps.filter(
+        ({ categories }) => intersection(categories, selectedCategories).length > 0,
+      )
+    );
+  }, [marketplaceApps, selectedCategories]);
+
+  return (
+    <Container>
+      <Filter>
+        <Typography variant="subtitle2" sx={{ mb: 3 }}>
+          Category
+        </Typography>
+        {categories &&
+          categories.map((category) => (
+            <FormGroup key={category} sx={{ mb: 2 }}>
+              <FormControlLabel
+                control={<Checkbox sx={{ mr: 2 }} onClick={() => onClickCategory(category)} />}
+                label={
+                  <Typography variant="body2" color={VOLCANIC_SAND}>
+                    {category}
+                  </Typography>
+                }
+                sx={{ ml: 0 }}
+              />
+            </FormGroup>
+          ))}
+      </Filter>
+      <Content>
+        <Typography variant="subtitle2">Featured</Typography>
+        <CardsContainer>
+          {filteredApps.map((app) => (
+            <MarketplaceCard key={app.name} {...app} onClick={() => handleSelectedApp(app)} />
+          ))}
+          <MarketplaceCard {...STATIC_HELP_CARD} showSubmitButton={false}>
+            <>
+              To suggest an open source app that installs to your cluster, discuss your idea via an{' '}
+              <NextLink href="https://github.com/kubefirst/kubefirst/issues" target="_blank">
+                issue
+              </NextLink>
+              . Learn how you can do this on our{' '}
+              <NextLink href="https://github.com/kubefirst/marketplace" target="_blank">
+                Contributing file
+              </NextLink>
+              .
+              <br />
+              <br />
+              Alternatively contact us via our{' '}
+              <NextLink href="https://kubefirst.io/slack" target="_blank">
+                Slack Community
+              </NextLink>{' '}
+              in the #helping-hands or #contributors channels.
+            </>
+          </MarketplaceCard>
+        </CardsContainer>
+      </Content>
+      {isOpen && selectedApp?.name && (
+        <MarketplaceModal closeModal={closeModal} isOpen={isOpen} {...selectedApp} />
+      )}
+    </Container>
+  );
+};
+
+export default Marketplace;
diff --git a/containers/marketplace/marketplace.styled.ts b/containers/marketplace/marketplace.styled.ts
new file mode 100644
index 00000000..d4938252
--- /dev/null
+++ b/containers/marketplace/marketplace.styled.ts
@@ -0,0 +1,31 @@
+import styled from 'styled-components';
+
+import Typography from '../../components/typography';
+
+export const CardsContainer = styled.div`
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+  margin-top: 24px;
+`;
+
+export const Container = styled.div`
+  display: flex;
+  height: calc(100% - 30px);
+  width: 100%;
+`;
+
+export const Content = styled.div`
+  padding: 24px;
+  width: 100%;
+`;
+
+export const Filter = styled.div`
+  background: ${({ theme }) => theme.colors.white};
+  border-width: 1px 1px 0px 1px;
+  border-style: solid;
+  border-color: ${({ theme }) => theme.colors.pastelLightBlue};
+  border-radius: 8px;
+  padding: 24px;
+  width: 266px;
+`;
diff --git a/containers/provision/index.tsx b/containers/provision/index.tsx
index a6aee970..e2d0db4a 100644
--- a/containers/provision/index.tsx
+++ b/containers/provision/index.tsx
@@ -1,7 +1,7 @@
 import React, { FunctionComponent, useCallback, useEffect, useMemo } from 'react';
 import { useForm } from 'react-hook-form';
 
-import { createCluster } from '../../redux/thunks/cluster.thunk';
+import { createCluster } from '../../redux/thunks/api.thunk';
 import InstallationStepContainer from '../../components/installationStepContainer';
 import InstallationInfoCard from '../../components/installationInfoCard';
 import { InstallationsSelection } from '../installationsSelection';
@@ -16,7 +16,7 @@ import { useInstallation } from '../../hooks/useInstallation';
 import { InstallValues, InstallationType } from '../../types/redux';
 import { GitProvider } from '../../types';
 import { setConfigValues } from '../../redux/slices/config.slice';
-import { clearClusterState } from '../../redux/slices/cluster.slice';
+import { clearClusterState } from '../../redux/slices/api.slice';
 import AdvancedOptions from '../clusterForms/shared/advancedOptions';
 import ErrorBanner from '../../components/errorBanner';
 import Button from '../../components/button';
@@ -34,7 +34,7 @@ const Provision: FunctionComponent<ProvisionProps> = ({ apiUrl, useTelemetry })
     ({ installation }) => installation,
   );
 
-  const { isProvisioned } = useAppSelector(({ cluster }) => cluster);
+  const { isProvisioned } = useAppSelector(({ api }) => api);
 
   const {
     stepTitles,
diff --git a/containers/services/index.tsx b/containers/services/index.tsx
index 91b53610..cef22cf3 100644
--- a/containers/services/index.tsx
+++ b/containers/services/index.tsx
@@ -1,16 +1,27 @@
-import React, { FunctionComponent, useEffect, useMemo, useCallback } from 'react';
+import React, { FunctionComponent, useEffect, useMemo, useCallback, useState } from 'react';
+import { Box, Tabs } from '@mui/material';
 
-import { DOCS_LINK } from '../../constants';
-import { setConfigValues } from '../../redux/slices/config.slice';
-import { GitProvider } from '../../types';
-import { useAppDispatch, useAppSelector } from '../../redux/store';
 import Service from '../service';
+import Marketplace from '../marketplace';
+import TabPanel, { Tab, a11yProps } from '../../components/tab';
 import Typography from '../../components/typography';
 import { useTelemetryMutation } from '../../redux/api';
+import { setConfigValues } from '../../redux/slices/config.slice';
+import { getMarketplaceApps } from '../../redux/thunks/api.thunk';
+import { useAppDispatch, useAppSelector } from '../../redux/store';
+import { GitProvider } from '../../types';
+import { DOCS_LINK } from '../../constants';
+import { BISCAY, SALTBOX_BLUE, VOLCANIC_SAND } from '../../constants/colors';
 
 import { Container, Header, LearnMoreLink, ServicesContainer } from './services.styled';
 
+enum SERVICES_TABS {
+  PROVISIONED = 0,
+  MARKETPLACE = 1,
+}
+
 export interface ServicesProps {
+  apiUrl: string;
   argoUrl: string;
   argoWorkflowsUrl: string;
   atlantisUrl: string;
@@ -30,6 +41,7 @@ export interface ServicesProps {
 }
 
 const Services: FunctionComponent<ServicesProps> = ({
+  apiUrl,
   argoUrl,
   argoWorkflowsUrl,
   atlantisUrl,
@@ -43,16 +55,13 @@ const Services: FunctionComponent<ServicesProps> = ({
   vaultUrl,
   metaphor,
 }) => {
+  const [activeTab, setActiveTab] = useState<number>(0);
   const [sendTelemetryEvent] = useTelemetryMutation();
 
   const isTelemetryEnabled = useAppSelector(({ config }) => config.isTelemetryEnabled);
 
   const dispatch = useAppDispatch();
 
-  useEffect(() => {
-    dispatch(setConfigValues({ isTelemetryEnabled: useTelemetry, kubefirstVersion, k3dDomain }));
-  }, [dispatch, useTelemetry, kubefirstVersion, k3dDomain]);
-
   const gitTileProvider = useMemo(
     () => (gitProvider === GitProvider.GITHUB ? 'GitHub' : 'GitLab'),
     [gitProvider],
@@ -123,11 +132,40 @@ const Services: FunctionComponent<ServicesProps> = ({
     [isTelemetryEnabled, sendTelemetryEvent],
   );
 
+  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
+    setActiveTab(newValue);
+  };
+
+  useEffect(() => {
+    dispatch(
+      setConfigValues({ isTelemetryEnabled: useTelemetry, kubefirstVersion, k3dDomain, apiUrl }),
+    );
+    dispatch(getMarketplaceApps());
+  }, [dispatch, useTelemetry, kubefirstVersion, k3dDomain, apiUrl]);
+
   return (
     <Container>
       <Header>
         <Typography variant="h6">Services Overview</Typography>
-        <Typography variant="body2">
+      </Header>
+      <Box sx={{ width: 'fit-content', mb: 4 }}>
+        <Tabs value={activeTab} onChange={handleChange} indicatorColor="primary">
+          <Tab
+            color={activeTab === SERVICES_TABS.PROVISIONED ? BISCAY : SALTBOX_BLUE}
+            label={<Typography variant="buttonSmall">Provisioned services</Typography>}
+            {...a11yProps(SERVICES_TABS.PROVISIONED)}
+            sx={{ textTransform: 'capitalize', mr: 3 }}
+          />
+          <Tab
+            color={activeTab === SERVICES_TABS.MARKETPLACE ? BISCAY : SALTBOX_BLUE}
+            label={<Typography variant="buttonSmall">Marketplace</Typography>}
+            {...a11yProps(SERVICES_TABS.MARKETPLACE)}
+            sx={{ textTransform: 'capitalize' }}
+          />
+        </Tabs>
+      </Box>
+      <TabPanel value={activeTab} index={SERVICES_TABS.PROVISIONED}>
+        <Typography variant="body2" sx={{ mb: 3 }} color={VOLCANIC_SAND}>
           Click on a link to access the service Kubefirst has provisioned for you.{' '}
           <LearnMoreLink
             href={DOCS_LINK}
@@ -137,18 +175,31 @@ const Services: FunctionComponent<ServicesProps> = ({
             Learn more
           </LearnMoreLink>
         </Typography>
-      </Header>
-      <ServicesContainer>
-        {services.map(({ name, ...rest }) => (
-          <Service
-            key={name}
-            name={name}
-            {...rest}
-            onClickLink={onClickLink}
-            domainName={domainName}
-          />
-        ))}
-      </ServicesContainer>
+        <ServicesContainer>
+          {services.map(({ name, ...rest }) => (
+            <Service
+              key={name}
+              name={name}
+              {...rest}
+              onClickLink={onClickLink}
+              domainName={domainName}
+            />
+          ))}
+        </ServicesContainer>
+      </TabPanel>
+      <TabPanel value={activeTab} index={SERVICES_TABS.MARKETPLACE}>
+        <Typography variant="body2" sx={{ mb: 3 }} color={VOLCANIC_SAND}>
+          Click on a link to access the service Kubefirst has provisioned for you.{' '}
+          <LearnMoreLink
+            href={DOCS_LINK}
+            target="_blank"
+            onClick={() => onClickLink(DOCS_LINK, 'docs')}
+          >
+            Learn more
+          </LearnMoreLink>
+        </Typography>
+        <Marketplace />
+      </TabPanel>
     </Container>
   );
 };
diff --git a/containers/services/services.styled.ts b/containers/services/services.styled.ts
index 6247ad2f..c7edd832 100644
--- a/containers/services/services.styled.ts
+++ b/containers/services/services.styled.ts
@@ -3,10 +3,9 @@ import styled from 'styled-components';
 
 export const Container = styled.div`
   height: calc(100vh - 80px);
-  overflow: auto;
   margin: 0 auto;
   padding: 40px;
-  max-width: 1192px;
+  width: 1192px;
 `;
 
 export const Header = styled.div`
@@ -14,7 +13,7 @@ export const Header = styled.div`
   display: flex;
   flex-direction: column;
   gap: 8px;
-  margin-bottom: 40px;
+  margin-bottom: 24px;
 `;
 
 export const LearnMoreLink = styled(Link)`
diff --git a/containers/terminalLogs/terminalLogs.tsx b/containers/terminalLogs/terminalLogs.tsx
index 8f3a8143..06af96ed 100644
--- a/containers/terminalLogs/terminalLogs.tsx
+++ b/containers/terminalLogs/terminalLogs.tsx
@@ -21,10 +21,10 @@ import ConciseLogs from '../conciseLogs';
 import useModal from '../../hooks/useModal';
 import Modal from '../../components/modal';
 import { useAppDispatch, useAppSelector } from '../../redux/store';
-import { getCluster } from '../../redux/thunks/cluster.thunk';
+import { getCluster } from '../../redux/thunks/api.thunk';
 import { ClusterRequestProps } from '../../types/provision';
 import { clearError, setError } from '../../redux/slices/installation.slice';
-import { setCompletedSteps } from '../../redux/slices/cluster.slice';
+import { setCompletedSteps } from '../../redux/slices/api.slice';
 import TabPanel, { Tab, a11yProps } from '../../components/tab';
 import FlappyKray from '../../components/flappyKray';
 import { CLUSTER_CHECKS } from '../../constants/cluster';
@@ -49,7 +49,7 @@ const TerminalLogs: FunctionComponent = () => {
   const dispatch = useAppDispatch();
   const {
     config: { apiUrl = '' },
-    cluster: {
+    api: {
       isProvisioned,
       isProvisioning,
       isError,
@@ -58,10 +58,10 @@ const TerminalLogs: FunctionComponent = () => {
       completedSteps,
     },
     installation: { values },
-  } = useAppSelector(({ config, cluster, installation }) => ({
+  } = useAppSelector(({ config, api, installation }) => ({
     installation,
     config,
-    cluster,
+    api,
   }));
 
   const { isOpen, openModal, closeModal } = useModal();
diff --git a/declaration.d.ts b/declaration.d.ts
index 0f1f22b2..6418b10b 100644
--- a/declaration.d.ts
+++ b/declaration.d.ts
@@ -61,6 +61,7 @@ declare module 'styled-components' {
       magnolia: string;
       royanPurple: string;
       sefidWhite: string;
+      pastelLightBlue: string;
 
       // Kubefirst color palette
       americanBlue: string;
diff --git a/next.config.js b/next.config.js
index bd8eecea..9a5b36db 100644
--- a/next.config.js
+++ b/next.config.js
@@ -17,6 +17,12 @@ const nextConfig = {
     contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
     remotePatterns: [],
     unoptimized: false,
+    remotePatterns: [
+      {
+        protocol: 'https',
+        hostname: '**',
+      },
+    ],
   },
   compiler: {
     styledComponents: true,
diff --git a/pages/services.tsx b/pages/services.tsx
index 1237084c..dc961161 100644
--- a/pages/services.tsx
+++ b/pages/services.tsx
@@ -5,6 +5,7 @@ import useFeatureFlag from '../hooks/useFeatureFlag';
 import Services from '../containers/services';
 
 interface ServicesPageProps {
+  apiUrl: string;
   argoUrl: string;
   argoWorkflowsUrl: string;
   atlantisUrl: string;
@@ -39,6 +40,7 @@ const ServicesPage: FunctionComponent<ServicesPageProps> = (props) => {
 
 export async function getServerSideProps() {
   const {
+    API_URL = '',
     ARGO_CD_URL = '',
     ARGO_WORKFLOWS_URL = '',
     ATLANTIS_URL = '',
@@ -57,6 +59,7 @@ export async function getServerSideProps() {
 
   return {
     props: {
+      apiUrl: API_URL,
       argoUrl: ARGO_CD_URL,
       argoWorkflowsUrl: ARGO_WORKFLOWS_URL,
       atlantisUrl: ATLANTIS_URL,
diff --git a/redux/api/index.ts b/redux/api/index.ts
index 9aa5b2c2..307d6c49 100644
--- a/redux/api/index.ts
+++ b/redux/api/index.ts
@@ -6,6 +6,7 @@ import { TelemetryResponseData } from '../../pages/api/telemetry';
 import { SendTelemetryArgs } from '../../services/telemetry';
 
 export const consoleApi = createApi({
+  reducerPath: 'internalApi',
   baseQuery: fetchBaseQuery({
     baseUrl: '',
   }),
diff --git a/redux/slices/cluster.slice.ts b/redux/slices/api.slice.ts
similarity index 82%
rename from redux/slices/cluster.slice.ts
rename to redux/slices/api.slice.ts
index a287a807..89e5bbd6 100644
--- a/redux/slices/cluster.slice.ts
+++ b/redux/slices/api.slice.ts
@@ -1,7 +1,14 @@
 import { PayloadAction, createSlice } from '@reduxjs/toolkit';
 
-import { createCluster, deleteCluster, getCluster, getClusters } from '../thunks/cluster.thunk';
+import {
+  createCluster,
+  deleteCluster,
+  getCluster,
+  getClusters,
+  getMarketplaceApps,
+} from '../thunks/api.thunk';
 import { Cluster, ClusterStatus } from '../../types/provision';
+import { MarketplaceApp } from '../../types/marketplace';
 
 export interface ApiState {
   loading: boolean;
@@ -15,6 +22,7 @@ export interface ApiState {
   clusters: Array<Cluster>;
   selectedCluster?: Cluster;
   completedSteps: Array<{ label: string; order: number }>;
+  marketplaceApps: Array<MarketplaceApp>;
 }
 
 export const initialState: ApiState = {
@@ -29,10 +37,11 @@ export const initialState: ApiState = {
   clusters: [],
   selectedCluster: undefined,
   completedSteps: [],
+  marketplaceApps: [],
 };
 
-const clusterSlice = createSlice({
-  name: 'cluster',
+const apiSlice = createSlice({
+  name: 'api',
   initialState,
   reducers: {
     setCompletedSteps: (state, action) => {
@@ -96,10 +105,16 @@ const clusterSlice = createSlice({
         state.loading = false;
         state.isError = false;
         state.clusters = payload;
-      });
+      })
+      .addCase(
+        getMarketplaceApps.fulfilled,
+        (state, { payload }: PayloadAction<Array<MarketplaceApp>>) => {
+          state.marketplaceApps = payload;
+        },
+      );
   },
 });
 
-export const { setCompletedSteps, clearClusterState } = clusterSlice.actions;
+export const { setCompletedSteps, clearClusterState } = apiSlice.actions;
 
-export const clusterReducer = clusterSlice.reducer;
+export const apiReducer = apiSlice.reducer;
diff --git a/redux/slices/index.ts b/redux/slices/index.ts
index 81e1cfdf..bf568181 100644
--- a/redux/slices/index.ts
+++ b/redux/slices/index.ts
@@ -2,5 +2,5 @@ export { readinessReducer } from './readiness.slice';
 export { gitReducer } from './git.slice';
 export { configReducer } from './config.slice';
 export { installationReducer } from './installation.slice';
-export { clusterReducer } from './cluster.slice';
+export { apiReducer } from './api.slice';
 export { featureFlagsReducer } from './featureFlags.slice';
diff --git a/redux/store.ts b/redux/store.ts
index a3205f00..2a485a4c 100644
--- a/redux/store.ts
+++ b/redux/store.ts
@@ -5,7 +5,7 @@ import { createWrapper } from 'next-redux-wrapper';
 import { consoleApi } from './api';
 import {
   configReducer,
-  clusterReducer,
+  apiReducer,
   featureFlagsReducer,
   gitReducer,
   installationReducer,
@@ -20,7 +20,7 @@ export const makeStore = () =>
       installation: installationReducer,
       git: gitReducer,
       readiness: readinessReducer,
-      cluster: clusterReducer,
+      api: apiReducer,
       featureFlags: featureFlagsReducer,
     },
     middleware: (gDM) => gDM().concat(consoleApi.middleware),
diff --git a/redux/thunks/cluster.thunk.ts b/redux/thunks/api.thunk.ts
similarity index 81%
rename from redux/thunks/cluster.thunk.ts
rename to redux/thunks/api.thunk.ts
index adb6c65a..ed200752 100644
--- a/redux/thunks/cluster.thunk.ts
+++ b/redux/thunks/api.thunk.ts
@@ -1,5 +1,6 @@
 import axios from 'axios';
 import { createAsyncThunk } from '@reduxjs/toolkit';
+import { MarketplaceApp } from 'types/marketplace';
 
 import { AppDispatch, RootState } from '../store';
 import { Cluster, ClusterRequestProps, ClusterResponse } from '../../types/provision';
@@ -47,7 +48,7 @@ export const createCluster = createAsyncThunk<
     dispatch: AppDispatch;
     state: RootState;
   }
->('cluster/provisioning', async ({ apiUrl }, { getState }) => {
+>('api/cluster/provisioning', async ({ apiUrl }, { getState }) => {
   const {
     installation: { installType, gitProvider, values },
   } = getState();
@@ -75,7 +76,10 @@ export const createCluster = createAsyncThunk<
       ...values?.vultr_auth,
     },
   };
-  const res = await axios.post(`${apiUrl}/cluster/${values?.clusterName || 'kubefirst'}`, params);
+  const res = await axios.post(
+    `${apiUrl}/api/cluster/${values?.clusterName || 'kubefirst'}`,
+    params,
+  );
 
   if ('error' in res) {
     throw res.error;
@@ -90,7 +94,7 @@ export const getCluster = createAsyncThunk<
     dispatch: AppDispatch;
     state: RootState;
   }
->('cluster/get', async ({ apiUrl, clusterName }) => {
+>('api/cluster/get', async ({ apiUrl, clusterName }) => {
   const res = await axios.get(`${apiUrl}/cluster/${clusterName || 'kubefirst'}`);
 
   if ('error' in res) {
@@ -106,7 +110,7 @@ export const getClusters = createAsyncThunk<
     dispatch: AppDispatch;
     state: RootState;
   }
->('cluster/getClusters', async ({ apiUrl }) => {
+>('api/cluster/getClusters', async ({ apiUrl }) => {
   const res = await axios.get(`${apiUrl}/cluster`);
 
   if ('error' in res) {
@@ -122,7 +126,7 @@ export const deleteCluster = createAsyncThunk<
     dispatch: AppDispatch;
     state: RootState;
   }
->('cluster/delete', async ({ apiUrl, clusterName }) => {
+>('api/cluster/delete', async ({ apiUrl, clusterName }) => {
   const res = await axios.delete(`${apiUrl}/cluster/${clusterName || 'kubefirst'}`);
 
   if ('error' in res) {
@@ -130,3 +134,23 @@ export const deleteCluster = createAsyncThunk<
   }
   return res.data;
 });
+
+export const getMarketplaceApps = createAsyncThunk<
+  Array<MarketplaceApp>,
+  void,
+  {
+    dispatch: AppDispatch;
+    state: RootState;
+  }
+>('api/getMarketplaceApps', async (_, { getState }) => {
+  const {
+    config: { apiUrl },
+  } = getState();
+
+  const res = await axios.get(`${apiUrl}/marketplace/apps`);
+
+  if ('error' in res) {
+    throw res.error;
+  }
+  return res.data.apps;
+});
diff --git a/theme/index.ts b/theme/index.ts
index 27388183..e40b65dd 100644
--- a/theme/index.ts
+++ b/theme/index.ts
@@ -37,6 +37,7 @@ import {
   MAGNOLIA,
   ROYAL_PURPLE,
   SEFID_WHITE,
+  PASTEL_LIGHT_BLUE,
 } from '../constants/colors';
 
 export const theme: DefaultTheme = {
@@ -77,5 +78,6 @@ export const theme: DefaultTheme = {
     magnolia: MAGNOLIA,
     royanPurple: ROYAL_PURPLE,
     sefidWhite: SEFID_WHITE,
+    pastelLightBlue: PASTEL_LIGHT_BLUE,
   },
 };
diff --git a/types/marketplace/index.ts b/types/marketplace/index.ts
new file mode 100644
index 00000000..1f2b01d3
--- /dev/null
+++ b/types/marketplace/index.ts
@@ -0,0 +1,7 @@
+export interface MarketplaceApp {
+  name: string;
+  secret_keys?: Array<string>;
+  image_url: string;
+  description?: string;
+  categories: Array<string>;
+}

From 2930fc53ffd7cb0b9b864069ac885b87f0f97f0c Mon Sep 17 00:00:00 2001
From: CristhianF7 <CristhianF7@gmail.com>
Date: Thu, 18 May 2023 22:51:40 -0500
Subject: [PATCH 2/6] feat: marketplace + api integration

---
 components/menu/index.tsx              |  2 +-
 components/service/index.tsx           | 22 +------
 containers/clusterManagement/index.tsx | 12 ++--
 containers/header/header.styled.ts     | 14 ++++
 containers/header/index.tsx            | 58 ++++++++++++++++
 containers/marketplace/index.tsx       | 20 ++++--
 containers/service/index.tsx           |  1 +
 containers/services/index.tsx          | 91 ++------------------------
 pages/_app.tsx                         |  9 +--
 pages/services.tsx                     | 20 ------
 redux/slices/api.slice.ts              | 19 +-----
 redux/slices/cluster.slice.ts          | 57 ++++++++++++++++
 redux/slices/index.ts                  |  1 +
 redux/store.ts                         |  4 +-
 redux/thunks/api.thunk.ts              | 55 +++++++++++++---
 types/marketplace/index.ts             |  5 ++
 types/provision/index.ts               |  9 +++
 17 files changed, 232 insertions(+), 167 deletions(-)
 create mode 100644 containers/header/header.styled.ts
 create mode 100644 containers/header/index.tsx
 create mode 100644 redux/slices/cluster.slice.ts

diff --git a/components/menu/index.tsx b/components/menu/index.tsx
index 9ae4dd36..f3ded3c9 100644
--- a/components/menu/index.tsx
+++ b/components/menu/index.tsx
@@ -8,7 +8,7 @@ import Typography from '../typography';
 import { VOLCANIC_SAND } from '../../constants/colors';
 
 export interface MenuProps {
-  isDisabled: boolean;
+  isDisabled?: boolean;
   label: string | ReactNode;
   options?: Array<{
     label: string;
diff --git a/components/service/index.tsx b/components/service/index.tsx
index 6a906799..cdf34ef1 100644
--- a/components/service/index.tsx
+++ b/components/service/index.tsx
@@ -1,13 +1,6 @@
 import React, { FunctionComponent, useCallback, useMemo } from 'react';
-import { StaticImageData } from 'next/image';
 import { Box, CircularProgress } from '@mui/material';
 
-import ArgoCDLogo from '../../assets/argocd.svg';
-import GitLabLogo from '../../assets/gitlab.svg';
-import GitHubLogo from '../../assets/github.svg';
-import VaultLogo from '../../assets/vault.svg';
-import AtlantisLogo from '../../assets/atlantis.svg';
-import MetaphorLogo from '../../assets/metaphor.svg';
 import Typography from '../typography';
 import { formatDomain } from '../../utils/url/formatDomain';
 import Tooltip from '../tooltip';
@@ -25,20 +18,11 @@ import {
   Title,
 } from './service.styled';
 
-const CARD_IMAGES: { [key: string]: StaticImageData } = {
-  ['Argo CD']: ArgoCDLogo,
-  ['Argo Workflows']: ArgoCDLogo,
-  ['GitLab']: GitLabLogo,
-  ['GitHub']: GitHubLogo,
-  ['Vault']: VaultLogo,
-  ['Atlantis']: AtlantisLogo,
-  ['Metaphor']: MetaphorLogo,
-};
-
 export interface ServiceProps {
   description?: string;
   domainName: string;
   children?: React.ReactNode;
+  image: string;
   name: string;
   links?: { [url: string]: boolean };
   onClickLink: (link: string, name: string) => void;
@@ -48,11 +32,11 @@ const Service: FunctionComponent<ServiceProps> = ({
   description,
   domainName,
   children,
+  image,
   name,
   links,
   onClickLink,
 }) => {
-  const serviceLogo = useMemo(() => CARD_IMAGES[name], [name]);
   const isMetaphor = useMemo(() => name === 'Metaphor', [name]);
 
   const serviceLink = useCallback(
@@ -118,7 +102,7 @@ const Service: FunctionComponent<ServiceProps> = ({
   return (
     <Container>
       <Header>
-        <Image src={serviceLogo} alt={name} width="24" />
+        <Image src={image} alt={name} width="24" height="24" />
         <Title variant="subtitle2">{name}</Title>
         {/* <NextImage
           src={`https://argocd.mgmt-20.kubefirst.com/api/badge?name=${name.toLowerCase()}`}
diff --git a/containers/clusterManagement/index.tsx b/containers/clusterManagement/index.tsx
index c8777228..43e6414f 100644
--- a/containers/clusterManagement/index.tsx
+++ b/containers/clusterManagement/index.tsx
@@ -124,11 +124,13 @@ const ClusterManagement: FunctionComponent<ClusterManagementProps> = ({ apiUrl,
         </Button>
       </Header>
       <Content>
-        <Table
-          columns={getClusterManagementColumns(handleMenuClick)}
-          rows={clusters}
-          getRowClassName={getClusterState}
-        />
+        {clusters && (
+          <Table
+            columns={getClusterManagementColumns(handleMenuClick)}
+            rows={clusters}
+            getRowClassName={getClusterState}
+          />
+        )}
       </Content>
       <Snackbar
         anchorOrigin={{
diff --git a/containers/header/header.styled.ts b/containers/header/header.styled.ts
new file mode 100644
index 00000000..59ea94c7
--- /dev/null
+++ b/containers/header/header.styled.ts
@@ -0,0 +1,14 @@
+import styled from 'styled-components';
+
+import row from '../../components/row';
+
+export const Container = styled(row)`
+  background-color: ${({ theme }) => theme.colors.white};
+  box-shadow: 0px 2px 4px rgba(31, 41, 55, 0.06);
+  height: 46px;
+  width: 100%;
+  z-index: 1500;
+`;
+
+export const ClusterIndicator = styled.div``;
+export const ClusterMenu = styled.div``;
diff --git a/containers/header/index.tsx b/containers/header/index.tsx
new file mode 100644
index 00000000..7e6c34d0
--- /dev/null
+++ b/containers/header/index.tsx
@@ -0,0 +1,58 @@
+import React, { FunctionComponent, useEffect } from 'react';
+import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
+import { setSelectedCluster } from 'redux/slices/cluster.slice';
+
+import { getClusters } from '../../redux/thunks/api.thunk';
+import Menu from '../../components/menu';
+import { useAppDispatch, useAppSelector } from '../../redux/store';
+
+import { ClusterIndicator, ClusterMenu, Container } from './header.styled';
+
+const Header: FunctionComponent = () => {
+  const dispatch = useAppDispatch();
+  const { apiUrl, clusters, selectedCluster } = useAppSelector(({ api, cluster, config }) => ({
+    clusters: api.clusters,
+    apiUrl: config.apiUrl,
+    selectedCluster: cluster.selectedCluster,
+  }));
+
+  const handleSelectCluster = (selectedClusterName: string) => {
+    const selectedCluster = clusters.find(({ clusterName }) => clusterName === selectedClusterName);
+
+    if (selectedCluster) {
+      dispatch(setSelectedCluster(selectedCluster));
+    }
+  };
+
+  useEffect(() => {
+    if (apiUrl) {
+      dispatch(getClusters({ apiUrl }));
+    }
+  }, [apiUrl, dispatch]);
+
+  useEffect(() => {
+    if (clusters.length && !selectedCluster) {
+      dispatch(setSelectedCluster(clusters[0]));
+    }
+  }, [clusters, clusters.length, dispatch, selectedCluster]);
+
+  return (
+    <Container>
+      {clusters?.length ? (
+        <Menu
+          onClickMenu={(cluster) => handleSelectCluster(cluster)}
+          label={
+            <ClusterMenu>
+              <ClusterIndicator />
+              {selectedCluster?.clusterName}
+              <KeyboardArrowDownIcon />
+            </ClusterMenu>
+          }
+          options={clusters && clusters.map(({ clusterName }) => ({ label: clusterName }))}
+        />
+      ) : null}
+    </Container>
+  );
+};
+
+export default Header;
diff --git a/containers/marketplace/index.tsx b/containers/marketplace/index.tsx
index 24901f6a..1492fb2f 100644
--- a/containers/marketplace/index.tsx
+++ b/containers/marketplace/index.tsx
@@ -17,8 +17,7 @@ import { CardsContainer, Container, Content, Filter } from './marketplace.styled
 const STATIC_HELP_CARD: MarketplaceApp = {
   categories: [],
   name: 'Can’t find what you need?',
-  image_url:
-    'https://raw.githubusercontent.com/kubefirst/kubefirst/main/images/kubefirst-light.svg',
+  image_url: 'https://assets.kubefirst.com/console/help.png',
 };
 
 const Marketplace: FunctionComponent = () => {
@@ -27,7 +26,7 @@ const Marketplace: FunctionComponent = () => {
 
   const { isOpen, openModal, closeModal } = useModal();
 
-  const marketplaceApps = useAppSelector(({ api }) => api.marketplaceApps);
+  const marketplaceApps = useAppSelector(({ cluster }) => cluster.marketplaceApps);
   const categories = useMemo(
     () =>
       marketplaceApps
@@ -75,6 +74,19 @@ const Marketplace: FunctionComponent = () => {
         <Typography variant="subtitle2" sx={{ mb: 3 }}>
           Category
         </Typography>
+        <FormGroup sx={{ mb: 2 }}>
+          <FormControlLabel
+            control={
+              <Checkbox sx={{ mr: 2 }} onClick={() => onClickCategory('all')} defaultChecked />
+            }
+            label={
+              <Typography variant="body2" color={VOLCANIC_SAND}>
+                All
+              </Typography>
+            }
+            sx={{ ml: 0 }}
+          />
+        </FormGroup>
         {categories &&
           categories.map((category) => (
             <FormGroup key={category} sx={{ mb: 2 }}>
@@ -91,7 +103,7 @@ const Marketplace: FunctionComponent = () => {
           ))}
       </Filter>
       <Content>
-        <Typography variant="subtitle2">Featured</Typography>
+        <Typography variant="subtitle2">All</Typography>
         <CardsContainer>
           {filteredApps.map((app) => (
             <MarketplaceCard key={app.name} {...app} onClick={() => handleSelectedApp(app)} />
diff --git a/containers/service/index.tsx b/containers/service/index.tsx
index b5efdfef..ad600ef4 100644
--- a/containers/service/index.tsx
+++ b/containers/service/index.tsx
@@ -8,6 +8,7 @@ export interface ServiceProps {
   description?: string;
   domainName: string;
   children?: React.ReactNode;
+  image: string;
   name: string;
   links?: Array<string>;
   onClickLink: (link: string, name: string) => void;
diff --git a/containers/services/index.tsx b/containers/services/index.tsx
index cef22cf3..a250cca9 100644
--- a/containers/services/index.tsx
+++ b/containers/services/index.tsx
@@ -1,4 +1,4 @@
-import React, { FunctionComponent, useEffect, useMemo, useCallback, useState } from 'react';
+import React, { FunctionComponent, useEffect, useCallback, useState } from 'react';
 import { Box, Tabs } from '@mui/material';
 
 import Service from '../service';
@@ -9,7 +9,6 @@ import { useTelemetryMutation } from '../../redux/api';
 import { setConfigValues } from '../../redux/slices/config.slice';
 import { getMarketplaceApps } from '../../redux/thunks/api.thunk';
 import { useAppDispatch, useAppSelector } from '../../redux/store';
-import { GitProvider } from '../../types';
 import { DOCS_LINK } from '../../constants';
 import { BISCAY, SALTBOX_BLUE, VOLCANIC_SAND } from '../../constants/colors';
 
@@ -22,106 +21,30 @@ enum SERVICES_TABS {
 
 export interface ServicesProps {
   apiUrl: string;
-  argoUrl: string;
-  argoWorkflowsUrl: string;
   atlantisUrl: string;
   domainName: string;
-  githubOwner: string;
-  gitlabOwner: string;
-  gitProvider: string;
   k3dDomain: string;
   kubefirstVersion: string;
   useTelemetry: boolean;
-  vaultUrl: string;
-  metaphor: {
-    development: string;
-    staging: string;
-    production: string;
-  };
 }
 
 const Services: FunctionComponent<ServicesProps> = ({
   apiUrl,
-  argoUrl,
-  argoWorkflowsUrl,
-  atlantisUrl,
   domainName,
-  githubOwner,
-  gitlabOwner,
-  gitProvider,
   k3dDomain,
   kubefirstVersion,
   useTelemetry,
-  vaultUrl,
-  metaphor,
 }) => {
   const [activeTab, setActiveTab] = useState<number>(0);
   const [sendTelemetryEvent] = useTelemetryMutation();
 
-  const isTelemetryEnabled = useAppSelector(({ config }) => config.isTelemetryEnabled);
+  const { isTelemetryEnabled, clusterServices } = useAppSelector(({ config, cluster }) => ({
+    isTelemetryEnabled: config.isTelemetryEnabled,
+    clusterServices: cluster.clusterServices,
+  }));
 
   const dispatch = useAppDispatch();
 
-  const gitTileProvider = useMemo(
-    () => (gitProvider === GitProvider.GITHUB ? 'GitHub' : 'GitLab'),
-    [gitProvider],
-  );
-
-  const gitLinks = useMemo(
-    () => [
-      gitProvider && `https://${gitProvider}.com/${githubOwner || gitlabOwner}/gitops`,
-      gitProvider && `https://${gitProvider}.com/${githubOwner || gitlabOwner}/metaphor`,
-    ],
-    [gitProvider, githubOwner, gitlabOwner],
-  );
-
-  const services = useMemo(
-    () => [
-      {
-        name: gitTileProvider,
-        description: `The ${gitTileProvider} repository contains all the Infrastructure as Code and GitOps configurations.`,
-        links: gitLinks,
-      },
-      {
-        name: 'Vault',
-        description: `Kubefirst’s secrets manager and identity provider.`,
-        links: [vaultUrl],
-      },
-      {
-        name: 'Argo CD',
-        description: `A GitOps oriented continuous delivery tool for managing all of our applications across our
-  kubernetes clusters.`,
-        links: [argoUrl],
-      },
-      {
-        name: 'Argo Workflows',
-        description: `The workflow engine for orchestrating parallel jobs on Kubernetes.`,
-        links: [`${argoWorkflowsUrl}/workflows`],
-      },
-      {
-        name: 'Atlantis',
-        description: `Kubefirst manages terraform workflows with atlantis automation.`,
-        links: [atlantisUrl],
-      },
-      {
-        name: 'Metaphor',
-        description: `A multi-environment demonstration space for frontend application best practices that’s easy to apply to other projects.`,
-        links: [metaphor?.development, metaphor?.staging, metaphor?.production],
-      },
-    ],
-    [
-      argoUrl,
-      argoWorkflowsUrl,
-      atlantisUrl,
-      gitLinks,
-      gitTileProvider,
-      metaphor?.development,
-      metaphor?.production,
-      metaphor?.staging,
-      vaultUrl,
-    ],
-  );
-
   const onClickLink = useCallback(
     (url: string, name: string) => {
       if (isTelemetryEnabled) {
@@ -176,7 +99,7 @@ const Services: FunctionComponent<ServicesProps> = ({
           </LearnMoreLink>
         </Typography>
         <ServicesContainer>
-          {services.map(({ name, ...rest }) => (
+          {clusterServices.map(({ name, ...rest }) => (
             <Service
               key={name}
               name={name}
@@ -189,7 +112,7 @@ const Services: FunctionComponent<ServicesProps> = ({
       </TabPanel>
       <TabPanel value={activeTab} index={SERVICES_TABS.MARKETPLACE}>
         <Typography variant="body2" sx={{ mb: 3 }} color={VOLCANIC_SAND}>
-          Click on a link to access the service Kubefirst has provisioned for you.{' '}
+          Add your favourite applications to your cluster.{' '}
           <LearnMoreLink
             href={DOCS_LINK}
             target="_blank"
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 389976b3..982fec3d 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -8,6 +8,7 @@ import styled, { ThemeProvider } from 'styled-components';
 import { muiTheme } from '../theme/muiTheme';
 import { theme } from '../theme';
 import { wrapper } from '../redux/store';
+import Header from '../containers/header';
 import Navigation from '../components/navigation';
 import Row from '../components/row';
 import Column from '../components/column';
@@ -20,14 +21,6 @@ const Layout = styled(Row)`
   width: 100vw;
 `;
 
-export const Header = styled(Row)`
-  background-color: ${({ theme }) => theme.colors.white};
-  box-shadow: 0px 2px 4px rgba(31, 41, 55, 0.06);
-  height: 46px;
-  width: 100%;
-  z-index: 1500;
-`;
-
 export const Content = styled(Column)`
   width: 100%;
 `;
diff --git a/pages/services.tsx b/pages/services.tsx
index dc961161..b1a7541f 100644
--- a/pages/services.tsx
+++ b/pages/services.tsx
@@ -41,18 +41,9 @@ const ServicesPage: FunctionComponent<ServicesPageProps> = (props) => {
 export async function getServerSideProps() {
   const {
     API_URL = '',
-    ARGO_CD_URL = '',
-    ARGO_WORKFLOWS_URL = '',
-    ATLANTIS_URL = '',
     DOMAIN_NAME = '',
-    GIT_PROVIDER = '',
-    GITHUB_OWNER = '',
-    GITLAB_OWNER = '',
     K3D_DOMAIN = '',
     KUBEFIRST_VERSION = '',
-    METAPHOR_DEVELOPMENT_URL = '',
-    METAPHOR_STAGING_URL = '',
-    METAPHOR_PRODUCTION_URL = '',
     USE_TELEMETRY = '',
     VAULT_URL = '',
   } = process.env;
@@ -60,22 +51,11 @@ export async function getServerSideProps() {
   return {
     props: {
       apiUrl: API_URL,
-      argoUrl: ARGO_CD_URL,
-      argoWorkflowsUrl: ARGO_WORKFLOWS_URL,
-      atlantisUrl: ATLANTIS_URL,
       domainName: DOMAIN_NAME,
-      githubOwner: GITHUB_OWNER,
-      gitlabOwner: GITLAB_OWNER,
-      gitProvider: GIT_PROVIDER,
       k3dDomain: K3D_DOMAIN,
       kubefirstVersion: KUBEFIRST_VERSION,
       useTelemetry: USE_TELEMETRY === 'true',
       vaultUrl: VAULT_URL,
-      metaphor: {
-        development: METAPHOR_DEVELOPMENT_URL,
-        staging: METAPHOR_STAGING_URL,
-        production: METAPHOR_PRODUCTION_URL,
-      },
     },
   };
 }
diff --git a/redux/slices/api.slice.ts b/redux/slices/api.slice.ts
index 89e5bbd6..9c93ab04 100644
--- a/redux/slices/api.slice.ts
+++ b/redux/slices/api.slice.ts
@@ -1,14 +1,7 @@
 import { PayloadAction, createSlice } from '@reduxjs/toolkit';
 
-import {
-  createCluster,
-  deleteCluster,
-  getCluster,
-  getClusters,
-  getMarketplaceApps,
-} from '../thunks/api.thunk';
+import { createCluster, deleteCluster, getCluster, getClusters } from '../thunks/api.thunk';
 import { Cluster, ClusterStatus } from '../../types/provision';
-import { MarketplaceApp } from '../../types/marketplace';
 
 export interface ApiState {
   loading: boolean;
@@ -22,7 +15,6 @@ export interface ApiState {
   clusters: Array<Cluster>;
   selectedCluster?: Cluster;
   completedSteps: Array<{ label: string; order: number }>;
-  marketplaceApps: Array<MarketplaceApp>;
 }
 
 export const initialState: ApiState = {
@@ -37,7 +29,6 @@ export const initialState: ApiState = {
   clusters: [],
   selectedCluster: undefined,
   completedSteps: [],
-  marketplaceApps: [],
 };
 
 const apiSlice = createSlice({
@@ -105,13 +96,7 @@ const apiSlice = createSlice({
         state.loading = false;
         state.isError = false;
         state.clusters = payload;
-      })
-      .addCase(
-        getMarketplaceApps.fulfilled,
-        (state, { payload }: PayloadAction<Array<MarketplaceApp>>) => {
-          state.marketplaceApps = payload;
-        },
-      );
+      });
   },
 });
 
diff --git a/redux/slices/cluster.slice.ts b/redux/slices/cluster.slice.ts
new file mode 100644
index 00000000..e0ceecb9
--- /dev/null
+++ b/redux/slices/cluster.slice.ts
@@ -0,0 +1,57 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+import {
+  getClusterServices,
+  getMarketplaceApps,
+  installMarketplaceApp,
+} from '../../redux/thunks/api.thunk';
+import { Cluster, ClusterServices } from '../../types/provision';
+import { MarketplaceApp } from '../../types/marketplace';
+
+export interface ConfigState {
+  selectedCluster?: Cluster;
+  clusterServices: Array<ClusterServices>;
+  marketplaceApps: Array<MarketplaceApp>;
+}
+
+export const initialState: ConfigState = {
+  selectedCluster: undefined,
+  clusterServices: [],
+  marketplaceApps: [],
+};
+
+const clusterSlice = createSlice({
+  name: 'cluster',
+  initialState,
+  reducers: {
+    setSelectedCluster: (state, { payload: cluster }: PayloadAction<Cluster>) => {
+      state.selectedCluster = cluster;
+    },
+  },
+  extraReducers: (builder) => {
+    builder
+      .addCase(getClusterServices.fulfilled, (state, { payload }) => {
+        state.clusterServices = payload;
+      })
+      .addCase(installMarketplaceApp.fulfilled, (state, { payload }) => {
+        const { name, description, image_url } = payload;
+        state.clusterServices.push({
+          default: false,
+          description: description as string,
+          name,
+          image: image_url,
+          links: [],
+        });
+      })
+      .addCase(
+        getMarketplaceApps.fulfilled,
+        (state, { payload }: PayloadAction<Array<MarketplaceApp>>) => {
+          state.marketplaceApps = payload;
+        },
+      );
+  },
+});
+
+export const { setSelectedCluster } = clusterSlice.actions;
+
+export const clusterReducer = clusterSlice.reducer;
diff --git a/redux/slices/index.ts b/redux/slices/index.ts
index bf568181..94421e1e 100644
--- a/redux/slices/index.ts
+++ b/redux/slices/index.ts
@@ -4,3 +4,4 @@ export { configReducer } from './config.slice';
 export { installationReducer } from './installation.slice';
 export { apiReducer } from './api.slice';
 export { featureFlagsReducer } from './featureFlags.slice';
+export { clusterReducer } from './cluster.slice';
diff --git a/redux/store.ts b/redux/store.ts
index 2a485a4c..83fecc58 100644
--- a/redux/store.ts
+++ b/redux/store.ts
@@ -4,8 +4,9 @@ import { createWrapper } from 'next-redux-wrapper';
 
 import { consoleApi } from './api';
 import {
-  configReducer,
   apiReducer,
+  clusterReducer,
+  configReducer,
   featureFlagsReducer,
   gitReducer,
   installationReducer,
@@ -22,6 +23,7 @@ export const makeStore = () =>
       readiness: readinessReducer,
       api: apiReducer,
       featureFlags: featureFlagsReducer,
+      cluster: clusterReducer,
     },
     middleware: (gDM) => gDM().concat(consoleApi.middleware),
   });
diff --git a/redux/thunks/api.thunk.ts b/redux/thunks/api.thunk.ts
index ed200752..7604d8f8 100644
--- a/redux/thunks/api.thunk.ts
+++ b/redux/thunks/api.thunk.ts
@@ -1,9 +1,14 @@
 import axios from 'axios';
 import { createAsyncThunk } from '@reduxjs/toolkit';
-import { MarketplaceApp } from 'types/marketplace';
 
 import { AppDispatch, RootState } from '../store';
-import { Cluster, ClusterRequestProps, ClusterResponse } from '../../types/provision';
+import {
+  Cluster,
+  ClusterRequestProps,
+  ClusterResponse,
+  ClusterServices,
+} from '../../types/provision';
+import { MarketplaceApp, MarketplaceProps } from '../../types/marketplace';
 
 const mapClusterFromRaw = (cluster: ClusterResponse): Cluster => ({
   id: cluster._id,
@@ -76,10 +81,7 @@ export const createCluster = createAsyncThunk<
       ...values?.vultr_auth,
     },
   };
-  const res = await axios.post(
-    `${apiUrl}/api/cluster/${values?.clusterName || 'kubefirst'}`,
-    params,
-  );
+  const res = await axios.post(`${apiUrl}/cluster/${values?.clusterName || 'kubefirst'}`, params);
 
   if ('error' in res) {
     throw res.error;
@@ -116,7 +118,8 @@ export const getClusters = createAsyncThunk<
   if ('error' in res) {
     throw res.error;
   }
-  return res.data.map(mapClusterFromRaw);
+
+  return (res.data && res.data.map(mapClusterFromRaw)) || [];
 });
 
 export const deleteCluster = createAsyncThunk<
@@ -135,6 +138,22 @@ export const deleteCluster = createAsyncThunk<
   return res.data;
 });
 
+export const getClusterServices = createAsyncThunk<
+  Array<ClusterServices>,
+  ClusterRequestProps,
+  {
+    dispatch: AppDispatch;
+    state: RootState;
+  }
+>('api/cluster/getClusterServices', async ({ apiUrl, clusterName }) => {
+  const res = await axios.get(`${apiUrl}/services/${clusterName}`);
+
+  if ('error' in res) {
+    throw res.error;
+  }
+  return res.data?.services;
+});
+
 export const getMarketplaceApps = createAsyncThunk<
   Array<MarketplaceApp>,
   void,
@@ -152,5 +171,25 @@ export const getMarketplaceApps = createAsyncThunk<
   if ('error' in res) {
     throw res.error;
   }
-  return res.data.apps;
+  return res.data?.apps;
+});
+
+export const installMarketplaceApp = createAsyncThunk<
+  MarketplaceApp,
+  MarketplaceProps,
+  {
+    dispatch: AppDispatch;
+    state: RootState;
+  }
+>('api/installMarketplaceApp', async ({ app, clusterName }, { getState }) => {
+  const {
+    config: { apiUrl },
+  } = getState();
+
+  const res = await axios.post(`${apiUrl}/services/${clusterName}/${app.name}`);
+
+  if ('error' in res) {
+    throw res.error;
+  }
+  return app;
 });
diff --git a/types/marketplace/index.ts b/types/marketplace/index.ts
index 1f2b01d3..719ce3b0 100644
--- a/types/marketplace/index.ts
+++ b/types/marketplace/index.ts
@@ -5,3 +5,8 @@ export interface MarketplaceApp {
   description?: string;
   categories: Array<string>;
 }
+
+export interface MarketplaceProps {
+  app: MarketplaceApp;
+  clusterName: string;
+}
diff --git a/types/provision/index.ts b/types/provision/index.ts
index e588f8cd..8b4dcc53 100644
--- a/types/provision/index.ts
+++ b/types/provision/index.ts
@@ -105,3 +105,12 @@ export interface Cluster extends Row {
     [key: string]: boolean;
   };
 }
+
+export interface ClusterServices {
+  name: string;
+  default: boolean;
+  description: string;
+  image: string;
+  links: Array<string>;
+  status?: string;
+}

From 5dd71a33171b2c516362deee02331d6fc73c9600 Mon Sep 17 00:00:00 2001
From: CristhianF7 <CristhianF7@gmail.com>
Date: Fri, 19 May 2023 01:08:25 -0500
Subject: [PATCH 3/6] feat: marketplace flow

---
 .../marketplaceCard/marketplaceCard.styled.ts |   3 +-
 components/marketplaceModal/index.tsx         |  30 ++++--
 components/service/service.styled.ts          |   1 +
 components/tab/tab.styled.ts                  |   2 +-
 containers/header/header.styled.ts            |  23 +++-
 containers/header/index.tsx                   |  12 ++-
 containers/marketplace/index.tsx              |  34 ++++--
 containers/marketplace/marketplace.styled.ts  |  10 +-
 containers/service/index.tsx                  |  66 ++++++------
 containers/services/index.tsx                 | 100 +++++++++++-------
 containers/services/services.styled.ts        |   8 +-
 redux/slices/cluster.slice.ts                 |  74 ++++++++++++-
 types/marketplace/index.ts                    |   2 +-
 13 files changed, 265 insertions(+), 100 deletions(-)

diff --git a/components/marketplaceCard/marketplaceCard.styled.ts b/components/marketplaceCard/marketplaceCard.styled.ts
index 7d24ab6f..a5061581 100644
--- a/components/marketplaceCard/marketplaceCard.styled.ts
+++ b/components/marketplaceCard/marketplaceCard.styled.ts
@@ -12,8 +12,9 @@ export const Card = styled.div`
 `;
 
 export const Description = styled(Typography)`
-  margin: 16px 0;
   color: ${({ theme }) => theme.colors.saltboxBlue};
+  height: 100px;
+  margin: 16px 0;
 
   & a {
     color: ${({ theme }) => theme.colors.primary};
diff --git a/components/marketplaceModal/index.tsx b/components/marketplaceModal/index.tsx
index fff0fe1e..7c92a84e 100644
--- a/components/marketplaceModal/index.tsx
+++ b/components/marketplaceModal/index.tsx
@@ -9,12 +9,15 @@ import Typography from '../typography';
 import ControlledPassword from '../controlledFields/Password';
 import { MarketplaceApp } from '../../types/marketplace';
 import { BISCAY, SALTBOX_BLUE } from '../../constants/colors';
+import { useAppDispatch } from '../../redux/store';
+import { addMarketplaceApp } from '../../redux/slices/cluster.slice';
 
 import { Content, Close, Footer, Header } from './marketplaceModal.styled';
 
 export interface MarketplaceModalProps extends MarketplaceApp {
   isOpen: boolean;
   closeModal: () => void;
+  onSubmit: (name: string) => void;
 }
 
 const MarketplaceModal: FunctionComponent<MarketplaceModalProps> = ({
@@ -23,8 +26,23 @@ const MarketplaceModal: FunctionComponent<MarketplaceModalProps> = ({
   name,
   image_url,
   secret_keys,
+  onSubmit,
+  ...rest
 }) => {
-  const { control } = useForm();
+  const dispatch = useAppDispatch();
+  const {
+    control,
+    formState: { isValid },
+  } = useForm();
+
+  const handleSubmit = () => {
+    setTimeout(() => {
+      dispatch(addMarketplaceApp({ name, image_url, secret_keys, ...rest }));
+      closeModal();
+      onSubmit(name);
+    }, 3000);
+  };
+
   return (
     <Modal isOpen={isOpen} padding={0}>
       <Box
@@ -46,12 +64,12 @@ const MarketplaceModal: FunctionComponent<MarketplaceModalProps> = ({
         <Divider />
         <Content>
           {secret_keys &&
-            secret_keys.map((key) => (
+            secret_keys.map(({ label, name }) => (
               <ControlledPassword
-                key={key}
+                key={label}
                 control={control}
-                name={key}
-                label={key}
+                name={name}
+                label={label}
                 rules={{
                   required: true,
                 }}
@@ -64,7 +82,7 @@ const MarketplaceModal: FunctionComponent<MarketplaceModalProps> = ({
           <Button variant="text" color="info" onClick={closeModal}>
             Cancel
           </Button>
-          <Button variant="contained" color="primary">
+          <Button variant="contained" color="primary" disabled={!isValid} onClick={handleSubmit}>
             Add
           </Button>
         </Footer>
diff --git a/components/service/service.styled.ts b/components/service/service.styled.ts
index 489dcf22..8ca77b25 100644
--- a/components/service/service.styled.ts
+++ b/components/service/service.styled.ts
@@ -54,6 +54,7 @@ export const Image = styled(NextImage)`
 export const Title = styled(Typography)`
   color: ${({ theme }) => theme.colors.volcanicSand};
   font-weight: 600;
+  text-transform: capitalize;
 `;
 
 export const Link = styled(NextLink)<{ disabled?: boolean }>`
diff --git a/components/tab/tab.styled.ts b/components/tab/tab.styled.ts
index 5bb03911..77d1a957 100644
--- a/components/tab/tab.styled.ts
+++ b/components/tab/tab.styled.ts
@@ -4,6 +4,6 @@ import { Box } from '@mui/material';
 export const TabContainer = styled(Box)<{ backgroundColor?: string }>`
   background: ${({ backgroundColor }) => backgroundColor};
   border-radius: 4px;
-  height: calc(100% - 122px);
+  height: 100%;
   width: 100%;
 `;
diff --git a/containers/header/header.styled.ts b/containers/header/header.styled.ts
index 59ea94c7..33c5414c 100644
--- a/containers/header/header.styled.ts
+++ b/containers/header/header.styled.ts
@@ -3,12 +3,31 @@ import styled from 'styled-components';
 import row from '../../components/row';
 
 export const Container = styled(row)`
+  align-items: center;
   background-color: ${({ theme }) => theme.colors.white};
   box-shadow: 0px 2px 4px rgba(31, 41, 55, 0.06);
+  display: flex;
   height: 46px;
+  justify-content: center;
   width: 100%;
   z-index: 1500;
 `;
 
-export const ClusterIndicator = styled.div``;
-export const ClusterMenu = styled.div``;
+export const ClusterIndicator = styled.div`
+  width: 8px;
+  height: 8px;
+  border-radius: 4px;
+  background: #22c55e;
+`;
+
+export const ClusterMenu = styled.div`
+  align-items: center;
+  display: flex;
+  gap: 8px;
+  justify-content: center;
+  text-transform: uppercase;
+
+  background: #fafafa;
+  border: 1px solid #f4f4f5;
+  padding: 0 8px;
+`;
diff --git a/containers/header/index.tsx b/containers/header/index.tsx
index 7e6c34d0..26542a0e 100644
--- a/containers/header/index.tsx
+++ b/containers/header/index.tsx
@@ -5,6 +5,8 @@ import { setSelectedCluster } from 'redux/slices/cluster.slice';
 import { getClusters } from '../../redux/thunks/api.thunk';
 import Menu from '../../components/menu';
 import { useAppDispatch, useAppSelector } from '../../redux/store';
+import Typography from '../../components/typography';
+import { SALTBOX_BLUE } from '../../constants/colors';
 
 import { ClusterIndicator, ClusterMenu, Container } from './header.styled';
 
@@ -38,19 +40,21 @@ const Header: FunctionComponent = () => {
 
   return (
     <Container>
-      {clusters?.length ? (
+      {/* {clusters?.length ? (
         <Menu
           onClickMenu={(cluster) => handleSelectCluster(cluster)}
           label={
             <ClusterMenu>
               <ClusterIndicator />
-              {selectedCluster?.clusterName}
-              <KeyboardArrowDownIcon />
+              <Typography variant="body2" color={SALTBOX_BLUE}>
+                {selectedCluster?.clusterName}
+              </Typography>
+              <KeyboardArrowDownIcon htmlColor={SALTBOX_BLUE} />
             </ClusterMenu>
           }
           options={clusters && clusters.map(({ clusterName }) => ({ label: clusterName }))}
         />
-      ) : null}
+      ) : null} */}
     </Container>
   );
 };
diff --git a/containers/marketplace/index.tsx b/containers/marketplace/index.tsx
index 1492fb2f..a1716c68 100644
--- a/containers/marketplace/index.tsx
+++ b/containers/marketplace/index.tsx
@@ -1,14 +1,16 @@
 import React, { FunctionComponent, useMemo, useState } from 'react';
 import { FormControlLabel, FormGroup } from '@mui/material';
 import intersection from 'lodash/intersection';
+import sortBy from 'lodash/sortBy';
 import NextLink from 'next/link';
+import { addMarketplaceApp } from 'redux/slices/cluster.slice';
 
 import Checkbox from '../../components/checkbox';
 import Typography from '../../components/typography';
 import MarketplaceCard from '../../components/marketplaceCard';
 import MarketplaceModal from '../../components/marketplaceModal';
 import useModal from '../../hooks/useModal';
-import { useAppSelector } from '../../redux/store';
+import { useAppDispatch, useAppSelector } from '../../redux/store';
 import { MarketplaceApp } from '../../types/marketplace';
 import { VOLCANIC_SAND } from '../../constants/colors';
 
@@ -20,13 +22,19 @@ const STATIC_HELP_CARD: MarketplaceApp = {
   image_url: 'https://assets.kubefirst.com/console/help.png',
 };
 
-const Marketplace: FunctionComponent = () => {
+const Marketplace: FunctionComponent<{ onSubmit: (name: string) => void }> = ({ onSubmit }) => {
   const [selectedCategories, setSelectedCategories] = useState<Array<string>>([]);
   const [selectedApp, setSelectedApp] = useState<MarketplaceApp>();
 
+  const dispatch = useAppDispatch();
+
   const { isOpen, openModal, closeModal } = useModal();
 
-  const marketplaceApps = useAppSelector(({ cluster }) => cluster.marketplaceApps);
+  const marketplaceApps = useAppSelector(({ cluster }) =>
+    cluster.marketplaceApps.filter(
+      (app) => !cluster.clusterServices.map((s) => s.name).includes(app.name),
+    ),
+  );
   const categories = useMemo(
     () =>
       marketplaceApps
@@ -51,8 +59,15 @@ const Marketplace: FunctionComponent = () => {
   };
 
   const handleSelectedApp = (app: MarketplaceApp) => {
-    setSelectedApp(app);
-    openModal();
+    if (app.secret_keys?.length) {
+      setSelectedApp(app);
+      openModal();
+    } else {
+      setTimeout(() => {
+        dispatch(addMarketplaceApp(app));
+        onSubmit(app.name);
+      }, 2000);
+    }
   };
 
   const filteredApps = useMemo(() => {
@@ -88,7 +103,7 @@ const Marketplace: FunctionComponent = () => {
           />
         </FormGroup>
         {categories &&
-          categories.map((category) => (
+          sortBy(categories).map((category) => (
             <FormGroup key={category} sx={{ mb: 2 }}>
               <FormControlLabel
                 control={<Checkbox sx={{ mr: 2 }} onClick={() => onClickCategory(category)} />}
@@ -131,7 +146,12 @@ const Marketplace: FunctionComponent = () => {
         </CardsContainer>
       </Content>
       {isOpen && selectedApp?.name && (
-        <MarketplaceModal closeModal={closeModal} isOpen={isOpen} {...selectedApp} />
+        <MarketplaceModal
+          closeModal={closeModal}
+          isOpen={isOpen}
+          onSubmit={onSubmit}
+          {...selectedApp}
+        />
       )}
     </Container>
   );
diff --git a/containers/marketplace/marketplace.styled.ts b/containers/marketplace/marketplace.styled.ts
index d4938252..cfce0a84 100644
--- a/containers/marketplace/marketplace.styled.ts
+++ b/containers/marketplace/marketplace.styled.ts
@@ -1,21 +1,22 @@
 import styled from 'styled-components';
 
-import Typography from '../../components/typography';
-
 export const CardsContainer = styled.div`
   display: flex;
   flex-wrap: wrap;
   gap: 16px;
   margin-top: 24px;
+  overflow: auto;
 `;
 
 export const Container = styled.div`
   display: flex;
-  height: calc(100% - 30px);
+  height: calc(100% - 80px);
   width: 100%;
 `;
 
 export const Content = styled.div`
+  height: calc(100% - 30px);
+  overflow: auto;
   padding: 24px;
   width: 100%;
 `;
@@ -26,6 +27,7 @@ export const Filter = styled.div`
   border-style: solid;
   border-color: ${({ theme }) => theme.colors.pastelLightBlue};
   border-radius: 8px;
-  padding: 24px;
+  height: 100%;
+  padding: 24px 24px 0 24px;
   width: 266px;
 `;
diff --git a/containers/service/index.tsx b/containers/service/index.tsx
index ad600ef4..da6a41f1 100644
--- a/containers/service/index.tsx
+++ b/containers/service/index.tsx
@@ -46,41 +46,41 @@ const Service: FunctionComponent<ServiceProps> = ({ links: serviceLinks, ...prop
     [dispatch, isSiteAvailable],
   );
 
-  useEffect(() => {
-    if (availableSites.length) {
-      setLinks(
-        links &&
-          Object.keys(links).reduce(
-            (previous, current) => ({ ...previous, [current]: isSiteAvailable(current) }),
-            {},
-          ),
-      );
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [availableSites]);
+  // useEffect(() => {
+  //   if (availableSites.length) {
+  //     setLinks(
+  //       links &&
+  //         Object.keys(links).reduce(
+  //           (previous, current) => ({ ...previous, [current]: isSiteAvailable(current) }),
+  //           {},
+  //         ),
+  //     );
+  //   }
+  //   // eslint-disable-next-line react-hooks/exhaustive-deps
+  // }, [availableSites]);
 
-  useEffect(() => {
-    const interval = setInterval(
-      () =>
-        links &&
-        Object.keys(links).map((url) => {
-          const isAvailable = links[url];
-          !isAvailable && checkSiteAvailability(url);
-        }),
-      20000,
-    );
-    return () => clearInterval(interval);
-  });
+  // useEffect(() => {
+  //   const interval = setInterval(
+  //     () =>
+  //       links &&
+  //       Object.keys(links).map((url) => {
+  //         const isAvailable = links[url];
+  //         !isAvailable && checkSiteAvailability(url);
+  //       }),
+  //     20000,
+  //   );
+  //   return () => clearInterval(interval);
+  // });
 
-  useEffect(() => {
-    if (!firstLoad) {
-      setFirstLoad(true);
-      links &&
-        Object.keys(links).map(async (url) => {
-          await checkSiteAvailability(url);
-        });
-    }
-  }, [checkSiteAvailability, dispatch, firstLoad, links]);
+  // useEffect(() => {
+  //   if (!firstLoad) {
+  //     setFirstLoad(true);
+  //     links &&
+  //       Object.keys(links).map(async (url) => {
+  //         await checkSiteAvailability(url);
+  //       });
+  //   }
+  // }, [checkSiteAvailability, dispatch, firstLoad, links]);
 
   return <ServiceComponent {...props} links={links} />;
 };
diff --git a/containers/services/index.tsx b/containers/services/index.tsx
index a250cca9..3823b18a 100644
--- a/containers/services/index.tsx
+++ b/containers/services/index.tsx
@@ -1,5 +1,5 @@
 import React, { FunctionComponent, useEffect, useCallback, useState } from 'react';
-import { Box, Tabs } from '@mui/material';
+import { Alert, Box, Snackbar, Tabs } from '@mui/material';
 
 import Service from '../service';
 import Marketplace from '../marketplace';
@@ -9,10 +9,11 @@ import { useTelemetryMutation } from '../../redux/api';
 import { setConfigValues } from '../../redux/slices/config.slice';
 import { getMarketplaceApps } from '../../redux/thunks/api.thunk';
 import { useAppDispatch, useAppSelector } from '../../redux/store';
+import useToggle from '../../hooks/useToggle';
 import { DOCS_LINK } from '../../constants';
 import { BISCAY, SALTBOX_BLUE, VOLCANIC_SAND } from '../../constants/colors';
 
-import { Container, Header, LearnMoreLink, ServicesContainer } from './services.styled';
+import { Container, Content, Header, LearnMoreLink, ServicesContainer } from './services.styled';
 
 enum SERVICES_TABS {
   PROVISIONED = 0,
@@ -35,7 +36,9 @@ const Services: FunctionComponent<ServicesProps> = ({
   kubefirstVersion,
   useTelemetry,
 }) => {
+  const [marketplaceApp, setMarketplaceApp] = useState<string>('');
   const [activeTab, setActiveTab] = useState<number>(0);
+  const { isOpen, open, close } = useToggle();
   const [sendTelemetryEvent] = useTelemetryMutation();
 
   const { isTelemetryEnabled, clusterServices } = useAppSelector(({ config, cluster }) => ({
@@ -87,42 +90,63 @@ const Services: FunctionComponent<ServicesProps> = ({
           />
         </Tabs>
       </Box>
-      <TabPanel value={activeTab} index={SERVICES_TABS.PROVISIONED}>
-        <Typography variant="body2" sx={{ mb: 3 }} color={VOLCANIC_SAND}>
-          Click on a link to access the service Kubefirst has provisioned for you.{' '}
-          <LearnMoreLink
-            href={DOCS_LINK}
-            target="_blank"
-            onClick={() => onClickLink(DOCS_LINK, 'docs')}
-          >
-            Learn more
-          </LearnMoreLink>
-        </Typography>
-        <ServicesContainer>
-          {clusterServices.map(({ name, ...rest }) => (
-            <Service
-              key={name}
-              name={name}
-              {...rest}
-              onClickLink={onClickLink}
-              domainName={domainName}
-            />
-          ))}
-        </ServicesContainer>
-      </TabPanel>
-      <TabPanel value={activeTab} index={SERVICES_TABS.MARKETPLACE}>
-        <Typography variant="body2" sx={{ mb: 3 }} color={VOLCANIC_SAND}>
-          Add your favourite applications to your cluster.{' '}
-          <LearnMoreLink
-            href={DOCS_LINK}
-            target="_blank"
-            onClick={() => onClickLink(DOCS_LINK, 'docs')}
-          >
-            Learn more
-          </LearnMoreLink>
-        </Typography>
-        <Marketplace />
-      </TabPanel>
+      <Content>
+        <TabPanel value={activeTab} index={SERVICES_TABS.PROVISIONED}>
+          <Typography variant="body2" sx={{ mb: 3 }} color={VOLCANIC_SAND}>
+            Click on a link to access the service Kubefirst has provisioned for you.{' '}
+            <LearnMoreLink
+              href={DOCS_LINK}
+              target="_blank"
+              onClick={() => onClickLink(DOCS_LINK, 'docs')}
+            >
+              Learn more
+            </LearnMoreLink>
+          </Typography>
+          <ServicesContainer>
+            {clusterServices.map(({ name, ...rest }) => (
+              <Service
+                key={name}
+                name={name}
+                {...rest}
+                onClickLink={onClickLink}
+                domainName={domainName}
+              />
+            ))}
+          </ServicesContainer>
+        </TabPanel>
+        <TabPanel value={activeTab} index={SERVICES_TABS.MARKETPLACE}>
+          <Typography variant="body2" sx={{ mb: 3 }} color={VOLCANIC_SAND}>
+            Add your favourite applications to your cluster.{' '}
+            <LearnMoreLink
+              href={DOCS_LINK}
+              target="_blank"
+              onClick={() => onClickLink(DOCS_LINK, 'docs')}
+            >
+              Learn more
+            </LearnMoreLink>
+          </Typography>
+          <Marketplace
+            onSubmit={(name: string) => {
+              setMarketplaceApp(name);
+              setActiveTab(SERVICES_TABS.PROVISIONED);
+              open();
+            }}
+          />
+        </TabPanel>
+      </Content>
+      <Snackbar
+        anchorOrigin={{
+          vertical: 'bottom',
+          horizontal: 'right',
+        }}
+        open={isOpen}
+        autoHideDuration={5000}
+        onClose={close}
+      >
+        <Alert onClose={close} severity="success" sx={{ width: '100%' }} variant="filled">
+          {`${marketplaceApp} successfully added to your cluster!`}
+        </Alert>
+      </Snackbar>
     </Container>
   );
 };
diff --git a/containers/services/services.styled.ts b/containers/services/services.styled.ts
index c7edd832..f241d3b4 100644
--- a/containers/services/services.styled.ts
+++ b/containers/services/services.styled.ts
@@ -4,10 +4,14 @@ import styled from 'styled-components';
 export const Container = styled.div`
   height: calc(100vh - 80px);
   margin: 0 auto;
-  padding: 40px;
+  padding-top: 40px;
   width: 1192px;
 `;
 
+export const Content = styled.div`
+  height: calc(100% - 140px);
+`;
+
 export const Header = styled.div`
   color: ${({ theme }) => theme.colors.volcanicSand};
   display: flex;
@@ -25,4 +29,6 @@ export const ServicesContainer = styled.div`
   display: flex;
   gap: 16px;
   flex-wrap: wrap;
+  max-height: calc(100% - 50px);
+  overflow: auto;
 `;
diff --git a/redux/slices/cluster.slice.ts b/redux/slices/cluster.slice.ts
index e0ceecb9..67dbe913 100644
--- a/redux/slices/cluster.slice.ts
+++ b/redux/slices/cluster.slice.ts
@@ -16,7 +16,66 @@ export interface ConfigState {
 
 export const initialState: ConfigState = {
   selectedCluster: undefined,
-  clusterServices: [],
+  clusterServices: [
+    {
+      name: 'Argo CD',
+      default: true,
+      description:
+        'A GitOps oriented continuous delivery tool for managing all of our applications across our Kubernetes clusters.',
+      image: 'https://assets.kubefirst.com/console/argocd.svg',
+      links: ['https://argocd.kubesecond.net'],
+      status: '',
+    },
+    {
+      name: 'Argo Workflows',
+      default: true,
+      description: 'The workflow engine for orchestrating parallel jobs on Kubernetes.',
+      image: 'https://assets.kubefirst.com/console/argocd.svg',
+      links: ['https://argo.kubesecond.net/workflows'],
+      status: '',
+    },
+    {
+      name: 'Atlantis',
+      default: true,
+      description: 'Kubefirst manages Terraform workflows with Atlantis automation.',
+      image: 'https://assets.kubefirst.com/console/atlantis.svg',
+      links: ['https://atlantis.kubesecond.net'],
+      status: '',
+    },
+    {
+      name: 'github',
+      default: true,
+      description:
+        'The git repositories contain all the Infrastructure as Code and GitOps configurations.',
+      image: 'https://assets.kubefirst.com/console/github.svg', // or gitlab https://assets.kubefirst.com/console/gitlab.svg
+      links: [
+        'https://github.com/kubefirst-test/gitops',
+        'https://github.com/kubefirst-test/metaphor',
+      ],
+      status: '',
+    },
+    {
+      name: 'Metaphor',
+      default: true,
+      description:
+        "A multi-environment demonstration space for frontend application best practices that's easy to apply to other projects.",
+      image: 'https://assets.kubefirst.com/console/metaphor.svg',
+      links: [
+        'https://metaphor-development.kubesecond.net',
+        'https://metaphor-staging.kubesecond.net',
+        'https://metaphor-production.kubesecond.net',
+      ],
+      status: '',
+    },
+    {
+      name: 'Vault',
+      default: true,
+      description: "Kubefirst's secrets manager and identity provider.",
+      image: 'https://assets.kubefirst.com/console/vault.svg',
+      links: ['https://vault.kubesecond.net'],
+      status: '',
+    },
+  ],
   marketplaceApps: [],
 };
 
@@ -27,6 +86,17 @@ const clusterSlice = createSlice({
     setSelectedCluster: (state, { payload: cluster }: PayloadAction<Cluster>) => {
       state.selectedCluster = cluster;
     },
+    addMarketplaceApp: (state, { payload: app }: PayloadAction<MarketplaceApp>) => {
+      console.log(app);
+      const { name, description, image_url } = app;
+      state.clusterServices.push({
+        default: false,
+        description: description as string,
+        name,
+        image: image_url,
+        links: [],
+      });
+    },
   },
   extraReducers: (builder) => {
     builder
@@ -52,6 +122,6 @@ const clusterSlice = createSlice({
   },
 });
 
-export const { setSelectedCluster } = clusterSlice.actions;
+export const { addMarketplaceApp, setSelectedCluster } = clusterSlice.actions;
 
 export const clusterReducer = clusterSlice.reducer;
diff --git a/types/marketplace/index.ts b/types/marketplace/index.ts
index 719ce3b0..f85c0508 100644
--- a/types/marketplace/index.ts
+++ b/types/marketplace/index.ts
@@ -1,6 +1,6 @@
 export interface MarketplaceApp {
   name: string;
-  secret_keys?: Array<string>;
+  secret_keys?: Array<{ name: string; label: string }>;
   image_url: string;
   description?: string;
   categories: Array<string>;

From a2eaf3a423ee4e370ecb2d96b34983f9f080d512 Mon Sep 17 00:00:00 2001
From: CristhianF7 <CristhianF7@gmail.com>
Date: Sun, 21 May 2023 22:22:18 -0500
Subject: [PATCH 4/6] feat: marketplace api integration refactor

---
 components/marketplaceModal/index.tsx        | 22 ++----
 components/menu/index.tsx                    |  1 +
 components/navigation/index.tsx              | 72 ++++++-------------
 components/table/index.tsx                   |  4 +-
 containers/clusterManagement/index.tsx       | 11 ++-
 containers/header/header.styled.ts           | 11 ++-
 containers/header/index.tsx                  |  8 ++-
 containers/marketplace/index.tsx             | 65 +++++++++++------
 containers/marketplace/marketplace.styled.ts |  1 +
 containers/navigation/index.tsx              | 75 ++++++++++++++++++++
 containers/provision/index.tsx               | 11 ++-
 containers/services/index.tsx                | 64 ++++++++---------
 pages/_app.tsx                               |  2 +-
 pages/cluster-management.tsx                 |  3 +-
 pages/index.ts                               | 12 ++--
 pages/provision.tsx                          |  9 +--
 pages/services.tsx                           | 32 +--------
 redux/slices/cluster.slice.ts                | 74 +------------------
 redux/thunks/api.thunk.ts                    |  6 +-
 19 files changed, 229 insertions(+), 254 deletions(-)
 create mode 100644 containers/navigation/index.tsx

diff --git a/components/marketplaceModal/index.tsx b/components/marketplaceModal/index.tsx
index 7c92a84e..1ab6bc5b 100644
--- a/components/marketplaceModal/index.tsx
+++ b/components/marketplaceModal/index.tsx
@@ -1,7 +1,7 @@
 import React, { FunctionComponent } from 'react';
 import { Box, Divider } from '@mui/material';
 import Image from 'next/image';
-import { useForm } from 'react-hook-form';
+import { Control } from 'react-hook-form';
 
 import Modal from '../modal';
 import Button from '../button';
@@ -9,38 +9,30 @@ import Typography from '../typography';
 import ControlledPassword from '../controlledFields/Password';
 import { MarketplaceApp } from '../../types/marketplace';
 import { BISCAY, SALTBOX_BLUE } from '../../constants/colors';
-import { useAppDispatch } from '../../redux/store';
-import { addMarketplaceApp } from '../../redux/slices/cluster.slice';
 
 import { Content, Close, Footer, Header } from './marketplaceModal.styled';
 
 export interface MarketplaceModalProps extends MarketplaceApp {
+  control: Control;
   isOpen: boolean;
+  isValid: boolean;
   closeModal: () => void;
-  onSubmit: (name: string) => void;
+  onSubmit: (app: MarketplaceApp) => void;
 }
 
 const MarketplaceModal: FunctionComponent<MarketplaceModalProps> = ({
+  control,
   closeModal,
   isOpen,
+  isValid,
   name,
   image_url,
   secret_keys,
   onSubmit,
   ...rest
 }) => {
-  const dispatch = useAppDispatch();
-  const {
-    control,
-    formState: { isValid },
-  } = useForm();
-
   const handleSubmit = () => {
-    setTimeout(() => {
-      dispatch(addMarketplaceApp({ name, image_url, secret_keys, ...rest }));
-      closeModal();
-      onSubmit(name);
-    }, 3000);
+    onSubmit({ name, image_url, secret_keys, ...rest });
   };
 
   return (
diff --git a/components/menu/index.tsx b/components/menu/index.tsx
index f3ded3c9..1872f692 100644
--- a/components/menu/index.tsx
+++ b/components/menu/index.tsx
@@ -44,6 +44,7 @@ const Menu: FunctionComponent<MenuProps> = ({ isDisabled, label, options, onClic
         onClick={handleClick}
         disableRipple
         disabled={isDisabled}
+        sx={{ padding: 0 }}
       >
         {label}
       </Button>
diff --git a/components/navigation/index.tsx b/components/navigation/index.tsx
index 07e7f854..e239ace4 100644
--- a/components/navigation/index.tsx
+++ b/components/navigation/index.tsx
@@ -1,16 +1,10 @@
-import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
+import React, { FunctionComponent, ReactNode } from 'react';
 import Image from 'next/image';
-import { useRouter } from 'next/router';
 import HelpIcon from '@mui/icons-material/Help';
-import ScatterPlotIcon from '@mui/icons-material/ScatterPlot';
-// import PeopleOutlineSharpIcon from '@mui/icons-material/PeopleOutlineSharp';
-import GridViewOutlinedIcon from '@mui/icons-material/GridViewOutlined';
 import { BsSlack } from 'react-icons/bs';
 import Link from 'next/link';
 
 import { ECHO_BLUE } from '../../constants/colors';
-import { useAppSelector } from '../../redux/store';
-import useFeatureFlag from '../../hooks/useFeatureFlag';
 
 import {
   Container,
@@ -35,50 +29,24 @@ const FOOTER_ITEMS = [
   },
 ];
 
-const Navigation: FunctionComponent = () => {
-  const [domLoaded, setDomLoaded] = useState(false);
-  const { asPath } = useRouter();
-  const { kubefirstVersion } = useAppSelector(({ config }) => config);
-  const { isEnabled, flagsAreReady } = useFeatureFlag('cluster-management');
-
-  const routes = useMemo(
-    () =>
-      [
-        {
-          icon: <ScatterPlotIcon />,
-          path: '/cluster-management',
-          title: 'Cluster Management',
-          isEnabled: flagsAreReady && isEnabled,
-        },
-        {
-          icon: <GridViewOutlinedIcon />,
-          path: '/services',
-          title: 'Services',
-          isEnabled: true,
-        },
-      ].filter(({ isEnabled }) => isEnabled),
-    [flagsAreReady, isEnabled],
-  );
-
-  const isActive = useCallback(
-    (route: string) => {
-      if (typeof window !== 'undefined') {
-        const linkPathname = new URL(route, window?.location?.href).pathname;
-
-        // Using URL().pathname to get rid of query and hash
-        const activePathname = new URL(asPath, window?.location?.href).pathname;
-
-        return linkPathname === activePathname;
-      }
-      return false;
-    },
-    [asPath],
-  );
-
-  useEffect(() => {
-    setDomLoaded(true);
-  }, []);
+export interface NavigationProps {
+  domLoaded: boolean;
+  handleIsActiveItem: (path: string) => void;
+  kubefirstVersion?: string;
+  routes: Array<{
+    icon: ReactNode;
+    path: string;
+    title: string;
+    isEnabled: boolean;
+  }>;
+}
 
+const Navigation: FunctionComponent<NavigationProps> = ({
+  domLoaded,
+  handleIsActiveItem,
+  kubefirstVersion,
+  routes,
+}) => {
   return (
     <Container>
       <div>
@@ -92,11 +60,11 @@ const Navigation: FunctionComponent = () => {
             </KubefirstVersion>
           )}
         </KubefirstTitle>
-        {domLoaded && flagsAreReady && (
+        {domLoaded && (
           <MenuContainer>
             {routes.map(({ icon, path, title }) => (
               <Link href={path} key={path}>
-                <MenuItem isActive={isActive(path)}>
+                <MenuItem isActive={handleIsActiveItem(path)}>
                   {icon}
                   <Title variant="body1">{title}</Title>
                 </MenuItem>
diff --git a/components/table/index.tsx b/components/table/index.tsx
index 645ecac7..cbe89176 100644
--- a/components/table/index.tsx
+++ b/components/table/index.tsx
@@ -57,8 +57,8 @@ const Table: FunctionComponent<DataGridProps> = ({ ...props }) => {
             padding: '0 16px',
           },
           [`.${gridClasses.main}`]: {
-            'background': 'white',
-            'border-radius': '4px',
+            background: 'white',
+            borderRadius: '4px',
           },
           'filter': 'drop-shadow(0px 4px 12px rgba(0, 0, 0, 0.04))',
           'border': 0,
diff --git a/containers/clusterManagement/index.tsx b/containers/clusterManagement/index.tsx
index 43e6414f..f525876f 100644
--- a/containers/clusterManagement/index.tsx
+++ b/containers/clusterManagement/index.tsx
@@ -24,9 +24,14 @@ import { getClusterManagementColumns, getClusterState } from './columnDefinition
 export interface ClusterManagementProps {
   apiUrl: string;
   useTelemetry: boolean;
+  kubefirstVersion: string;
 }
 
-const ClusterManagement: FunctionComponent<ClusterManagementProps> = ({ apiUrl, useTelemetry }) => {
+const ClusterManagement: FunctionComponent<ClusterManagementProps> = ({
+  apiUrl,
+  kubefirstVersion,
+  useTelemetry,
+}) => {
   const [selectedCluster, setSelectedCluster] = useState<Cluster>();
   const {
     isOpen: isDetailsPanelOpen,
@@ -104,8 +109,8 @@ const ClusterManagement: FunctionComponent<ClusterManagementProps> = ({ apiUrl,
   }, [apiUrl, dispatch, handleGetClusters]);
 
   useEffect(() => {
-    dispatch(setConfigValues({ isTelemetryEnabled: useTelemetry, apiUrl }));
-  }, [dispatch, useTelemetry, apiUrl]);
+    dispatch(setConfigValues({ isTelemetryEnabled: useTelemetry, apiUrl, kubefirstVersion }));
+  }, [dispatch, useTelemetry, apiUrl, kubefirstVersion]);
 
   return (
     <Container>
diff --git a/containers/header/header.styled.ts b/containers/header/header.styled.ts
index 33c5414c..a60e7e5c 100644
--- a/containers/header/header.styled.ts
+++ b/containers/header/header.styled.ts
@@ -7,10 +7,9 @@ export const Container = styled(row)`
   background-color: ${({ theme }) => theme.colors.white};
   box-shadow: 0px 2px 4px rgba(31, 41, 55, 0.06);
   display: flex;
-  height: 46px;
+  min-height: 64px;
   justify-content: center;
   width: 100%;
-  z-index: 1500;
 `;
 
 export const ClusterIndicator = styled.div`
@@ -22,12 +21,12 @@ export const ClusterIndicator = styled.div`
 
 export const ClusterMenu = styled.div`
   align-items: center;
+  background: #fafafa;
+  border: 1px solid #f4f4f5;
   display: flex;
+  height: 28px;
   gap: 8px;
   justify-content: center;
-  text-transform: uppercase;
-
-  background: #fafafa;
-  border: 1px solid #f4f4f5;
   padding: 0 8px;
+  text-transform: uppercase;
 `;
diff --git a/containers/header/index.tsx b/containers/header/index.tsx
index 26542a0e..fbb31b91 100644
--- a/containers/header/index.tsx
+++ b/containers/header/index.tsx
@@ -40,7 +40,7 @@ const Header: FunctionComponent = () => {
 
   return (
     <Container>
-      {/* {clusters?.length ? (
+      {clusters?.length ? (
         <Menu
           onClickMenu={(cluster) => handleSelectCluster(cluster)}
           label={
@@ -52,9 +52,11 @@ const Header: FunctionComponent = () => {
               <KeyboardArrowDownIcon htmlColor={SALTBOX_BLUE} />
             </ClusterMenu>
           }
-          options={clusters && clusters.map(({ clusterName }) => ({ label: clusterName }))}
+          options={
+            clusters && clusters.map(({ clusterName }) => ({ label: clusterName.toUpperCase() }))
+          }
         />
-      ) : null} */}
+      ) : null}
     </Container>
   );
 };
diff --git a/containers/marketplace/index.tsx b/containers/marketplace/index.tsx
index a1716c68..76a80646 100644
--- a/containers/marketplace/index.tsx
+++ b/containers/marketplace/index.tsx
@@ -1,16 +1,18 @@
 import React, { FunctionComponent, useMemo, useState } from 'react';
-import { FormControlLabel, FormGroup } from '@mui/material';
+import { useForm } from 'react-hook-form';
+import NextLink from 'next/link';
 import intersection from 'lodash/intersection';
 import sortBy from 'lodash/sortBy';
-import NextLink from 'next/link';
-import { addMarketplaceApp } from 'redux/slices/cluster.slice';
+import { Alert, FormControlLabel, FormGroup, Snackbar } from '@mui/material';
 
 import Checkbox from '../../components/checkbox';
 import Typography from '../../components/typography';
 import MarketplaceCard from '../../components/marketplaceCard';
 import MarketplaceModal from '../../components/marketplaceModal';
 import useModal from '../../hooks/useModal';
+import useToggle from '../../hooks/useToggle';
 import { useAppDispatch, useAppSelector } from '../../redux/store';
+import { installMarketplaceApp } from '../../redux/thunks/api.thunk';
 import { MarketplaceApp } from '../../types/marketplace';
 import { VOLCANIC_SAND } from '../../constants/colors';
 
@@ -22,19 +24,27 @@ const STATIC_HELP_CARD: MarketplaceApp = {
   image_url: 'https://assets.kubefirst.com/console/help.png',
 };
 
-const Marketplace: FunctionComponent<{ onSubmit: (name: string) => void }> = ({ onSubmit }) => {
+const Marketplace: FunctionComponent<{ onSubmit: () => void }> = ({ onSubmit }) => {
   const [selectedCategories, setSelectedCategories] = useState<Array<string>>([]);
   const [selectedApp, setSelectedApp] = useState<MarketplaceApp>();
 
   const dispatch = useAppDispatch();
+  const selectedCluster = useAppSelector(({ cluster }) => cluster.selectedCluster);
 
   const { isOpen, openModal, closeModal } = useModal();
+  const { isOpen: isNotificationOpen, open, close } = useToggle();
+
+  const {
+    control,
+    formState: { isValid },
+  } = useForm();
 
   const marketplaceApps = useAppSelector(({ cluster }) =>
     cluster.marketplaceApps.filter(
       (app) => !cluster.clusterServices.map((s) => s.name).includes(app.name),
     ),
   );
+
   const categories = useMemo(
     () =>
       marketplaceApps
@@ -58,15 +68,24 @@ const Marketplace: FunctionComponent<{ onSubmit: (name: string) => void }> = ({
     }
   };
 
+  const handleAddMarketplaceApp = async (app: MarketplaceApp) => {
+    try {
+      await dispatch(
+        installMarketplaceApp({ app, clusterName: selectedCluster?.clusterName as string }),
+      );
+      open();
+      onSubmit();
+    } catch (error) {
+      //todo: handle error
+    }
+  };
+
   const handleSelectedApp = (app: MarketplaceApp) => {
     if (app.secret_keys?.length) {
       setSelectedApp(app);
       openModal();
     } else {
-      setTimeout(() => {
-        dispatch(addMarketplaceApp(app));
-        onSubmit(app.name);
-      }, 2000);
+      handleAddMarketplaceApp(app);
     }
   };
 
@@ -89,19 +108,6 @@ const Marketplace: FunctionComponent<{ onSubmit: (name: string) => void }> = ({
         <Typography variant="subtitle2" sx={{ mb: 3 }}>
           Category
         </Typography>
-        <FormGroup sx={{ mb: 2 }}>
-          <FormControlLabel
-            control={
-              <Checkbox sx={{ mr: 2 }} onClick={() => onClickCategory('all')} defaultChecked />
-            }
-            label={
-              <Typography variant="body2" color={VOLCANIC_SAND}>
-                All
-              </Typography>
-            }
-            sx={{ ml: 0 }}
-          />
-        </FormGroup>
         {categories &&
           sortBy(categories).map((category) => (
             <FormGroup key={category} sx={{ mb: 2 }}>
@@ -147,12 +153,27 @@ const Marketplace: FunctionComponent<{ onSubmit: (name: string) => void }> = ({
       </Content>
       {isOpen && selectedApp?.name && (
         <MarketplaceModal
+          control={control}
+          isValid={isValid}
           closeModal={closeModal}
           isOpen={isOpen}
-          onSubmit={onSubmit}
+          onSubmit={handleAddMarketplaceApp}
           {...selectedApp}
         />
       )}
+      <Snackbar
+        anchorOrigin={{
+          vertical: 'bottom',
+          horizontal: 'right',
+        }}
+        open={isNotificationOpen}
+        autoHideDuration={5000}
+        onClose={close}
+      >
+        <Alert onClose={close} severity="success" sx={{ width: '100%' }} variant="filled">
+          {`${selectedApp?.name} successfully added to your cluster!`}
+        </Alert>
+      </Snackbar>
     </Container>
   );
 };
diff --git a/containers/marketplace/marketplace.styled.ts b/containers/marketplace/marketplace.styled.ts
index cfce0a84..032006c6 100644
--- a/containers/marketplace/marketplace.styled.ts
+++ b/containers/marketplace/marketplace.styled.ts
@@ -28,6 +28,7 @@ export const Filter = styled.div`
   border-color: ${({ theme }) => theme.colors.pastelLightBlue};
   border-radius: 8px;
   height: 100%;
+  overflow: auto;
   padding: 24px 24px 0 24px;
   width: 266px;
 `;
diff --git a/containers/navigation/index.tsx b/containers/navigation/index.tsx
new file mode 100644
index 00000000..b6c13bc6
--- /dev/null
+++ b/containers/navigation/index.tsx
@@ -0,0 +1,75 @@
+import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
+import { useRouter } from 'next/router';
+import ScatterPlotIcon from '@mui/icons-material/ScatterPlot';
+import GridViewOutlinedIcon from '@mui/icons-material/GridViewOutlined';
+
+import NavigationComponent from '../../components/navigation';
+import useFeatureFlag from '../../hooks/useFeatureFlag';
+import { useAppSelector } from '../../redux/store';
+
+const Navigation: FunctionComponent = () => {
+  const [domLoaded, setDomLoaded] = useState<boolean>(false);
+  const { asPath } = useRouter();
+  const { kubefirstVersion, selectedCluster } = useAppSelector(({ config, cluster }) => ({
+    kubefirstVersion: config.kubefirstVersion,
+    selectedCluster: cluster.selectedCluster,
+  }));
+  const { isEnabled, flagsAreReady } = useFeatureFlag('cluster-management');
+  const { isEnabled: isClusterProvisioningEnabled } = useFeatureFlag('cluster-provisioning');
+
+  const routes = useMemo(
+    () =>
+      [
+        {
+          icon: <ScatterPlotIcon />,
+          path: '/cluster-management',
+          title: 'Cluster Management',
+          isEnabled: flagsAreReady && isEnabled,
+        },
+        {
+          icon: <ScatterPlotIcon />,
+          path: '/provision',
+          title: 'Cluster Provisioning',
+          isEnabled: flagsAreReady && isClusterProvisioningEnabled,
+        },
+        {
+          icon: <GridViewOutlinedIcon />,
+          path: '/services',
+          title: 'Services',
+          isEnabled: !!selectedCluster?.clusterName,
+        },
+      ].filter(({ isEnabled }) => isEnabled),
+    [flagsAreReady, isClusterProvisioningEnabled, isEnabled, selectedCluster?.clusterName],
+  );
+
+  const handleIsActiveItem = useCallback(
+    (route: string) => {
+      if (typeof window !== 'undefined') {
+        const linkPathname = new URL(route, window?.location?.href).pathname;
+
+        // Using URL().pathname to get rid of query and hash
+        const activePathname = new URL(asPath, window?.location?.href).pathname;
+
+        return linkPathname === activePathname;
+      }
+
+      return false;
+    },
+    [asPath],
+  );
+
+  useEffect(() => {
+    setDomLoaded(true);
+  }, []);
+
+  return (
+    <NavigationComponent
+      domLoaded={domLoaded}
+      kubefirstVersion={kubefirstVersion}
+      routes={routes}
+      handleIsActiveItem={handleIsActiveItem}
+    />
+  );
+};
+
+export default Navigation;
diff --git a/containers/provision/index.tsx b/containers/provision/index.tsx
index e2d0db4a..7f56e88f 100644
--- a/containers/provision/index.tsx
+++ b/containers/provision/index.tsx
@@ -25,10 +25,15 @@ import { AdvancedOptionsContainer, ErrorContainer, Form, FormContent } from './p
 
 export interface ProvisionProps {
   apiUrl: string;
+  kubefirstVersion: string;
   useTelemetry: boolean;
 }
 
-const Provision: FunctionComponent<ProvisionProps> = ({ apiUrl, useTelemetry }) => {
+const Provision: FunctionComponent<ProvisionProps> = ({
+  apiUrl,
+  kubefirstVersion,
+  useTelemetry,
+}) => {
   const dispatch = useAppDispatch();
   const { installType, gitProvider, installationStep, values, error } = useAppSelector(
     ({ installation }) => installation,
@@ -188,12 +193,12 @@ const Provision: FunctionComponent<ProvisionProps> = ({ apiUrl, useTelemetry })
   ]);
 
   useEffect(() => {
-    dispatch(setConfigValues({ isTelemetryEnabled: useTelemetry, apiUrl }));
+    dispatch(setConfigValues({ isTelemetryEnabled: useTelemetry, apiUrl, kubefirstVersion }));
 
     return () => {
       dispatch(resetInstallState());
     };
-  }, [dispatch, useTelemetry, apiUrl]);
+  }, [dispatch, useTelemetry, apiUrl, kubefirstVersion]);
 
   return (
     <Form component="form" onSubmit={handleSubmit(onSubmit)}>
diff --git a/containers/services/index.tsx b/containers/services/index.tsx
index 3823b18a..b91ed039 100644
--- a/containers/services/index.tsx
+++ b/containers/services/index.tsx
@@ -1,15 +1,16 @@
 import React, { FunctionComponent, useEffect, useCallback, useState } from 'react';
-import { Alert, Box, Snackbar, Tabs } from '@mui/material';
+import { Box, Tabs } from '@mui/material';
+import { useRouter } from 'next/router';
 
 import Service from '../service';
 import Marketplace from '../marketplace';
 import TabPanel, { Tab, a11yProps } from '../../components/tab';
 import Typography from '../../components/typography';
+import useFeatureFlag from '../../hooks/useFeatureFlag';
 import { useTelemetryMutation } from '../../redux/api';
 import { setConfigValues } from '../../redux/slices/config.slice';
-import { getMarketplaceApps } from '../../redux/thunks/api.thunk';
+import { getClusterServices, getMarketplaceApps } from '../../redux/thunks/api.thunk';
 import { useAppDispatch, useAppSelector } from '../../redux/store';
-import useToggle from '../../hooks/useToggle';
 import { DOCS_LINK } from '../../constants';
 import { BISCAY, SALTBOX_BLUE, VOLCANIC_SAND } from '../../constants/colors';
 
@@ -22,7 +23,6 @@ enum SERVICES_TABS {
 
 export interface ServicesProps {
   apiUrl: string;
-  atlantisUrl: string;
   domainName: string;
   k3dDomain: string;
   kubefirstVersion: string;
@@ -36,17 +36,20 @@ const Services: FunctionComponent<ServicesProps> = ({
   kubefirstVersion,
   useTelemetry,
 }) => {
-  const [marketplaceApp, setMarketplaceApp] = useState<string>('');
   const [activeTab, setActiveTab] = useState<number>(0);
-  const { isOpen, open, close } = useToggle();
   const [sendTelemetryEvent] = useTelemetryMutation();
+  const router = useRouter();
 
-  const { isTelemetryEnabled, clusterServices } = useAppSelector(({ config, cluster }) => ({
-    isTelemetryEnabled: config.isTelemetryEnabled,
-    clusterServices: cluster.clusterServices,
-  }));
+  const { isEnabled: isMarketplaceEnabled, flagsAreReady } = useFeatureFlag('marketplace');
 
   const dispatch = useAppDispatch();
+  const { clusterServices, isTelemetryEnabled, selectedCluster } = useAppSelector(
+    ({ config, cluster }) => ({
+      isTelemetryEnabled: config.isTelemetryEnabled,
+      clusterServices: cluster.clusterServices,
+      selectedCluster: cluster.selectedCluster,
+    }),
+  );
 
   const onClickLink = useCallback(
     (url: string, name: string) => {
@@ -69,6 +72,16 @@ const Services: FunctionComponent<ServicesProps> = ({
     dispatch(getMarketplaceApps());
   }, [dispatch, useTelemetry, kubefirstVersion, k3dDomain, apiUrl]);
 
+  useEffect(() => {
+    if (!selectedCluster?.clusterName) {
+      router.push('/');
+    } else {
+      dispatch(() => {
+        dispatch(getClusterServices({ clusterName: selectedCluster?.clusterName }));
+      });
+    }
+  }, [dispatch, router, selectedCluster]);
+
   return (
     <Container>
       <Header>
@@ -82,12 +95,14 @@ const Services: FunctionComponent<ServicesProps> = ({
             {...a11yProps(SERVICES_TABS.PROVISIONED)}
             sx={{ textTransform: 'capitalize', mr: 3 }}
           />
-          <Tab
-            color={activeTab === SERVICES_TABS.MARKETPLACE ? BISCAY : SALTBOX_BLUE}
-            label={<Typography variant="buttonSmall">Marketplace</Typography>}
-            {...a11yProps(SERVICES_TABS.MARKETPLACE)}
-            sx={{ textTransform: 'capitalize' }}
-          />
+          {isMarketplaceEnabled && flagsAreReady && (
+            <Tab
+              color={activeTab === SERVICES_TABS.MARKETPLACE ? BISCAY : SALTBOX_BLUE}
+              label={<Typography variant="buttonSmall">Marketplace</Typography>}
+              {...a11yProps(SERVICES_TABS.MARKETPLACE)}
+              sx={{ textTransform: 'capitalize' }}
+            />
+          )}
         </Tabs>
       </Box>
       <Content>
@@ -126,27 +141,12 @@ const Services: FunctionComponent<ServicesProps> = ({
             </LearnMoreLink>
           </Typography>
           <Marketplace
-            onSubmit={(name: string) => {
-              setMarketplaceApp(name);
+            onSubmit={() => {
               setActiveTab(SERVICES_TABS.PROVISIONED);
-              open();
             }}
           />
         </TabPanel>
       </Content>
-      <Snackbar
-        anchorOrigin={{
-          vertical: 'bottom',
-          horizontal: 'right',
-        }}
-        open={isOpen}
-        autoHideDuration={5000}
-        onClose={close}
-      >
-        <Alert onClose={close} severity="success" sx={{ width: '100%' }} variant="filled">
-          {`${marketplaceApp} successfully added to your cluster!`}
-        </Alert>
-      </Snackbar>
     </Container>
   );
 };
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 982fec3d..f1371852 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -9,7 +9,7 @@ import { muiTheme } from '../theme/muiTheme';
 import { theme } from '../theme';
 import { wrapper } from '../redux/store';
 import Header from '../containers/header';
-import Navigation from '../components/navigation';
+import Navigation from '../containers/navigation';
 import Row from '../components/row';
 import Column from '../components/column';
 
diff --git a/pages/cluster-management.tsx b/pages/cluster-management.tsx
index 43b3269f..ff635c73 100644
--- a/pages/cluster-management.tsx
+++ b/pages/cluster-management.tsx
@@ -23,11 +23,12 @@ const ClusterManagementPage: FunctionComponent<ClusterManagementProps> = (props)
 };
 
 export async function getServerSideProps() {
-  const { API_URL = '', USE_TELEMETRY = '' } = process.env;
+  const { API_URL = '', KUBEFIRST_VERSION = '', USE_TELEMETRY = '' } = process.env;
 
   return {
     props: {
       apiUrl: API_URL,
+      kubefirstVersion: KUBEFIRST_VERSION,
       useTelemetry: USE_TELEMETRY === 'true',
     },
   };
diff --git a/pages/index.ts b/pages/index.ts
index a2f29fe9..57570e7f 100644
--- a/pages/index.ts
+++ b/pages/index.ts
@@ -15,8 +15,8 @@ const MainPage: FunctionComponent<MainPageProps> = ({ flags }) => {
   const { push } = useRouter();
 
   const dispatch = useAppDispatch();
-  const { isEnabled: clusterManagementEnabled, flagsAreReady } =
-    useFeatureFlag('cluster-management');
+  const { isEnabled: clusterProvisioningEnabled, flagsAreReady } =
+    useFeatureFlag('cluster-provisioning');
 
   useEffect(() => {
     dispatch(setFeatureFlags(flags));
@@ -24,13 +24,11 @@ const MainPage: FunctionComponent<MainPageProps> = ({ flags }) => {
 
   useEffect(() => {
     if (flagsAreReady) {
-      if (clusterManagementEnabled) {
-        push('/cluster-management');
-      } else {
-        push('/services');
+      if (clusterProvisioningEnabled) {
+        push('/provision');
       }
     }
-  }, [clusterManagementEnabled, flagsAreReady, push]);
+  }, [clusterProvisioningEnabled, flagsAreReady, push]);
 
   return null;
 };
diff --git a/pages/provision.tsx b/pages/provision.tsx
index c4d27857..c83a3922 100644
--- a/pages/provision.tsx
+++ b/pages/provision.tsx
@@ -4,10 +4,10 @@ import { useRouter } from 'next/router';
 import useFeatureFlag from '../hooks/useFeatureFlag';
 import Provision, { ProvisionProps } from '../containers/provision';
 
-const ProvisionPage: FunctionComponent<ProvisionProps> = ({ apiUrl, useTelemetry }) => {
+const ProvisionPage: FunctionComponent<ProvisionProps> = (props) => {
   const { push } = useRouter();
 
-  const { flagsAreReady } = useFeatureFlag('cluster-management');
+  const { flagsAreReady } = useFeatureFlag('cluster-provisioning');
 
   useEffect(() => {
     if (!flagsAreReady) {
@@ -19,15 +19,16 @@ const ProvisionPage: FunctionComponent<ProvisionProps> = ({ apiUrl, useTelemetry
     return null;
   }
 
-  return <Provision apiUrl={apiUrl} useTelemetry={useTelemetry} />;
+  return <Provision {...props} />;
 };
 
 export async function getServerSideProps() {
-  const { API_URL = '', USE_TELEMETRY = '' } = process.env;
+  const { API_URL = '', KUBEFIRST_VERSION = '', USE_TELEMETRY = '' } = process.env;
 
   return {
     props: {
       apiUrl: API_URL,
+      kubefirstVersion: KUBEFIRST_VERSION,
       useTelemetry: USE_TELEMETRY === 'true',
     },
   };
diff --git a/pages/services.tsx b/pages/services.tsx
index b1a7541f..43293152 100644
--- a/pages/services.tsx
+++ b/pages/services.tsx
@@ -1,42 +1,16 @@
-import React, { FunctionComponent, useEffect } from 'react';
-import { useRouter } from 'next/router';
+import React, { FunctionComponent } from 'react';
 
-import useFeatureFlag from '../hooks/useFeatureFlag';
 import Services from '../containers/services';
 
 interface ServicesPageProps {
   apiUrl: string;
-  argoUrl: string;
-  argoWorkflowsUrl: string;
-  atlantisUrl: string;
   domainName: string;
-  githubOwner: string;
-  gitlabOwner: string;
-  gitProvider: string;
   k3dDomain: string;
   kubefirstVersion: string;
   useTelemetry: boolean;
-  vaultUrl: string;
-  metaphor: {
-    development: string;
-    staging: string;
-    production: string;
-  };
 }
 
-const ServicesPage: FunctionComponent<ServicesPageProps> = (props) => {
-  const { push } = useRouter();
-
-  const { flagsAreReady } = useFeatureFlag('cluster-management');
-
-  useEffect(() => {
-    if (!flagsAreReady) {
-      push('/');
-    }
-  });
-
-  return <Services {...props} />;
-};
+const ServicesPage: FunctionComponent<ServicesPageProps> = (props) => <Services {...props} />;
 
 export async function getServerSideProps() {
   const {
@@ -45,7 +19,6 @@ export async function getServerSideProps() {
     K3D_DOMAIN = '',
     KUBEFIRST_VERSION = '',
     USE_TELEMETRY = '',
-    VAULT_URL = '',
   } = process.env;
 
   return {
@@ -55,7 +28,6 @@ export async function getServerSideProps() {
       k3dDomain: K3D_DOMAIN,
       kubefirstVersion: KUBEFIRST_VERSION,
       useTelemetry: USE_TELEMETRY === 'true',
-      vaultUrl: VAULT_URL,
     },
   };
 }
diff --git a/redux/slices/cluster.slice.ts b/redux/slices/cluster.slice.ts
index 67dbe913..e0ceecb9 100644
--- a/redux/slices/cluster.slice.ts
+++ b/redux/slices/cluster.slice.ts
@@ -16,66 +16,7 @@ export interface ConfigState {
 
 export const initialState: ConfigState = {
   selectedCluster: undefined,
-  clusterServices: [
-    {
-      name: 'Argo CD',
-      default: true,
-      description:
-        'A GitOps oriented continuous delivery tool for managing all of our applications across our Kubernetes clusters.',
-      image: 'https://assets.kubefirst.com/console/argocd.svg',
-      links: ['https://argocd.kubesecond.net'],
-      status: '',
-    },
-    {
-      name: 'Argo Workflows',
-      default: true,
-      description: 'The workflow engine for orchestrating parallel jobs on Kubernetes.',
-      image: 'https://assets.kubefirst.com/console/argocd.svg',
-      links: ['https://argo.kubesecond.net/workflows'],
-      status: '',
-    },
-    {
-      name: 'Atlantis',
-      default: true,
-      description: 'Kubefirst manages Terraform workflows with Atlantis automation.',
-      image: 'https://assets.kubefirst.com/console/atlantis.svg',
-      links: ['https://atlantis.kubesecond.net'],
-      status: '',
-    },
-    {
-      name: 'github',
-      default: true,
-      description:
-        'The git repositories contain all the Infrastructure as Code and GitOps configurations.',
-      image: 'https://assets.kubefirst.com/console/github.svg', // or gitlab https://assets.kubefirst.com/console/gitlab.svg
-      links: [
-        'https://github.com/kubefirst-test/gitops',
-        'https://github.com/kubefirst-test/metaphor',
-      ],
-      status: '',
-    },
-    {
-      name: 'Metaphor',
-      default: true,
-      description:
-        "A multi-environment demonstration space for frontend application best practices that's easy to apply to other projects.",
-      image: 'https://assets.kubefirst.com/console/metaphor.svg',
-      links: [
-        'https://metaphor-development.kubesecond.net',
-        'https://metaphor-staging.kubesecond.net',
-        'https://metaphor-production.kubesecond.net',
-      ],
-      status: '',
-    },
-    {
-      name: 'Vault',
-      default: true,
-      description: "Kubefirst's secrets manager and identity provider.",
-      image: 'https://assets.kubefirst.com/console/vault.svg',
-      links: ['https://vault.kubesecond.net'],
-      status: '',
-    },
-  ],
+  clusterServices: [],
   marketplaceApps: [],
 };
 
@@ -86,17 +27,6 @@ const clusterSlice = createSlice({
     setSelectedCluster: (state, { payload: cluster }: PayloadAction<Cluster>) => {
       state.selectedCluster = cluster;
     },
-    addMarketplaceApp: (state, { payload: app }: PayloadAction<MarketplaceApp>) => {
-      console.log(app);
-      const { name, description, image_url } = app;
-      state.clusterServices.push({
-        default: false,
-        description: description as string,
-        name,
-        image: image_url,
-        links: [],
-      });
-    },
   },
   extraReducers: (builder) => {
     builder
@@ -122,6 +52,6 @@ const clusterSlice = createSlice({
   },
 });
 
-export const { addMarketplaceApp, setSelectedCluster } = clusterSlice.actions;
+export const { setSelectedCluster } = clusterSlice.actions;
 
 export const clusterReducer = clusterSlice.reducer;
diff --git a/redux/thunks/api.thunk.ts b/redux/thunks/api.thunk.ts
index 7604d8f8..a5818958 100644
--- a/redux/thunks/api.thunk.ts
+++ b/redux/thunks/api.thunk.ts
@@ -145,7 +145,11 @@ export const getClusterServices = createAsyncThunk<
     dispatch: AppDispatch;
     state: RootState;
   }
->('api/cluster/getClusterServices', async ({ apiUrl, clusterName }) => {
+>('api/cluster/getClusterServices', async ({ clusterName }, { getState }) => {
+  const {
+    config: { apiUrl },
+  } = getState();
+
   const res = await axios.get(`${apiUrl}/services/${clusterName}`);
 
   if ('error' in res) {

From 2811693849d79578d71ffa1a24587504ce03e3c2 Mon Sep 17 00:00:00 2001
From: CristhianF7 <CristhianF7@gmail.com>
Date: Tue, 23 May 2023 17:34:21 -0500
Subject: [PATCH 5/6] feat: error handling and git validations

---
 components/errorBanner/errorBanner.styled.ts  | 11 +++
 components/errorBanner/index.tsx              | 32 ++++++--
 .../clusterForms/shared/authForm/index.tsx    | 52 ++----------
 containers/header/index.tsx                   |  4 +-
 containers/marketplace/index.tsx              | 12 ++-
 containers/provision/index.tsx                | 18 +++--
 containers/provision/provision.styled.ts      |  2 +
 containers/service/index.tsx                  | 66 +++++++--------
 containers/services/index.tsx                 |  6 +-
 containers/services/services.styled.ts        |  2 +-
 containers/terminalLogs/terminalLogs.tsx      | 22 +++--
 hooks/useInstallation.ts                      |  2 +-
 redux/slices/git.slice.ts                     | 81 ++++++++++---------
 redux/thunks/api.thunk.ts                     | 18 ++++-
 types/marketplace/index.ts                    |  3 +
 types/redux/index.ts                          |  2 +-
 16 files changed, 180 insertions(+), 153 deletions(-)

diff --git a/components/errorBanner/errorBanner.styled.ts b/components/errorBanner/errorBanner.styled.ts
index 01f184d8..555a7905 100644
--- a/components/errorBanner/errorBanner.styled.ts
+++ b/components/errorBanner/errorBanner.styled.ts
@@ -9,7 +9,18 @@ export const Container = styled.div`
   width: calc(100% - 32px);
 `;
 
+export const ErrorContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+`;
+
 export const Header = styled.div`
   display: flex;
   gap: 8px;
 `;
+
+export const List = styled.ul`
+  padding-left: 28px;
+`;
+
+export const ListItem = styled.li``;
diff --git a/components/errorBanner/index.tsx b/components/errorBanner/index.tsx
index be759822..fea07339 100644
--- a/components/errorBanner/index.tsx
+++ b/components/errorBanner/index.tsx
@@ -4,21 +4,41 @@ import ErrorIcon from '@mui/icons-material/Error';
 import Typography from '../typography';
 import { VOLCANIC_SAND } from '../../constants/colors';
 
-import { Container, Header } from './errorBanner.styled';
+import { Container, ErrorContainer, Header, List, ListItem } from './errorBanner.styled';
 
 export interface ErrorBannerProps {
   details?: string;
-  text: string;
+  error: Array<string> | string;
 }
 
-const ErrorBanner: FunctionComponent<ErrorBannerProps> = ({ text }) => {
+const ErrorBanner: FunctionComponent<ErrorBannerProps> = ({ error }) => {
+  const isErrorArray = Array.isArray(error) && error.length > 1;
   return (
     <Container>
       <Header>
         <ErrorIcon color="error" fontSize="small" />
-        <Typography variant="body2" color={VOLCANIC_SAND}>
-          <div dangerouslySetInnerHTML={{ __html: text }} />
-        </Typography>
+        <ErrorContainer>
+          {isErrorArray ? (
+            <>
+              <Typography variant="body2" color={VOLCANIC_SAND}>
+                <strong>Error</strong>
+              </Typography>
+              <List>
+                {error.map((errorItem) => (
+                  <ListItem key={errorItem}>
+                    <Typography variant="body2" color={VOLCANIC_SAND}>
+                      <div dangerouslySetInnerHTML={{ __html: errorItem }} />
+                    </Typography>
+                  </ListItem>
+                ))}
+              </List>
+            </>
+          ) : (
+            <Typography variant="body2" color={VOLCANIC_SAND}>
+              <div dangerouslySetInnerHTML={{ __html: `<strong>Error </strong>${error}` }} />
+            </Typography>
+          )}
+        </ErrorContainer>
       </Header>
     </Container>
   );
diff --git a/containers/clusterForms/shared/authForm/index.tsx b/containers/clusterForms/shared/authForm/index.tsx
index 3cb67a71..37ddd720 100644
--- a/containers/clusterForms/shared/authForm/index.tsx
+++ b/containers/clusterForms/shared/authForm/index.tsx
@@ -6,7 +6,6 @@ import LearnMore from '../../../../components/learnMore';
 import ControlledPassword from '../../../../components/controlledFields/Password';
 import ControlledTextField from '../../../../components/controlledFields/TextField';
 import ControlledAutocomplete from '../../../../components/controlledFields/AutoComplete';
-import { clearError, setError } from '../../../../redux/slices/installation.slice';
 import { useAppDispatch, useAppSelector } from '../../../../redux/store';
 import { GitProvider } from '../../../../types';
 import { FormFlowProps } from '../../../../types/provision';
@@ -21,11 +20,10 @@ import {
   getGitlabGroups,
   getGitlabUser,
 } from '../../../../redux/thunks/git.thunk';
-import { clearGitValidationState, setToken } from '../../../../redux/slices/git.slice';
+import { setToken, clearUserError, setGitOwner } from '../../../../redux/slices/git.slice';
 
 const AuthForm: FunctionComponent<FormFlowProps<InstallValues>> = ({ control, setValue }) => {
   const [isGitRequested, setIsGitRequested] = useState<boolean>();
-  const [selectedGitOwner, setSelectedGitOwner] = useState<string>();
   const dispatch = useAppDispatch();
 
   const {
@@ -38,10 +36,6 @@ const AuthForm: FunctionComponent<FormFlowProps<InstallValues>> = ({ control, se
     installationType,
     isTokenValid,
     token = '',
-    hasExistingRepos,
-    hasExistingTeams,
-    loadedRepositories,
-    loadedTeams,
   } = useAppSelector(({ installation, git }) => ({
     currentStep: installation.installationStep,
     installationType: installation.installType,
@@ -58,9 +52,8 @@ const AuthForm: FunctionComponent<FormFlowProps<InstallValues>> = ({ control, se
   const isGitHub = useMemo(() => gitProvider === GitProvider.GITHUB, [gitProvider]);
 
   const validateGitOwner = async (gitOwner: string) => {
-    setSelectedGitOwner(gitOwner);
-    await dispatch(clearError());
-    await dispatch(clearGitValidationState());
+    dispatch(setGitOwner(gitOwner));
+    await dispatch(clearUserError());
     if (gitOwner) {
       if (isGitHub) {
         await dispatch(getGitHubOrgRepositories({ token, organization: gitOwner })).unwrap();
@@ -72,8 +65,7 @@ const AuthForm: FunctionComponent<FormFlowProps<InstallValues>> = ({ control, se
   };
 
   const handleGitTokenBlur = async (token: string) => {
-    await dispatch(clearGitValidationState());
-    await dispatch(clearError());
+    await dispatch(clearUserError());
     await dispatch(setToken(token));
 
     try {
@@ -113,42 +105,10 @@ const AuthForm: FunctionComponent<FormFlowProps<InstallValues>> = ({ control, se
   );
 
   useEffect(() => {
-    if (loadedRepositories && loadedTeams && selectedGitOwner) {
-      if (hasExistingRepos) {
-        dispatch(
-          setError({
-            error: `<strong>Error </strong>${gitErrorLabel}<strong> ${selectedGitOwner} </strong>
-              already has a ${
-                isGitHub ? 'repository' : 'project'
-              } named <strong>gitops</strong> or <strong>metaphor</strong>. 
-              Please remove or rename them to continue.`,
-          }),
-        );
-      } else if (hasExistingTeams) {
-        dispatch(
-          setError({
-            error: `<strong>Error</strong>${gitErrorLabel} <strong> ${selectedGitOwner} </strong> 
-            already has a team named <strong>admins</strong> or <strong>developers</strong>. 
-            Please remove or rename them to continue.`,
-          }),
-        );
-      }
-    }
-
     return () => {
-      dispatch(clearError());
-      // dispatch(clearGitValidationState());
+      dispatch(clearUserError());
     };
-  }, [
-    dispatch,
-    gitErrorLabel,
-    hasExistingRepos,
-    hasExistingTeams,
-    isGitHub,
-    loadedRepositories,
-    loadedTeams,
-    selectedGitOwner,
-  ]);
+  }, [dispatch, gitErrorLabel, isGitHub]);
 
   return (
     <>
diff --git a/containers/header/index.tsx b/containers/header/index.tsx
index fbb31b91..048e3c3f 100644
--- a/containers/header/index.tsx
+++ b/containers/header/index.tsx
@@ -19,7 +19,9 @@ const Header: FunctionComponent = () => {
   }));
 
   const handleSelectCluster = (selectedClusterName: string) => {
-    const selectedCluster = clusters.find(({ clusterName }) => clusterName === selectedClusterName);
+    const selectedCluster = clusters.find(
+      ({ clusterName }) => clusterName.toLowerCase() === selectedClusterName.toLowerCase(),
+    );
 
     if (selectedCluster) {
       dispatch(setSelectedCluster(selectedCluster));
diff --git a/containers/marketplace/index.tsx b/containers/marketplace/index.tsx
index 76a80646..bddaf6c5 100644
--- a/containers/marketplace/index.tsx
+++ b/containers/marketplace/index.tsx
@@ -1,5 +1,5 @@
 import React, { FunctionComponent, useMemo, useState } from 'react';
-import { useForm } from 'react-hook-form';
+import { useForm, FieldValues } from 'react-hook-form';
 import NextLink from 'next/link';
 import intersection from 'lodash/intersection';
 import sortBy from 'lodash/sortBy';
@@ -24,7 +24,7 @@ const STATIC_HELP_CARD: MarketplaceApp = {
   image_url: 'https://assets.kubefirst.com/console/help.png',
 };
 
-const Marketplace: FunctionComponent<{ onSubmit: () => void }> = ({ onSubmit }) => {
+const Marketplace: FunctionComponent = () => {
   const [selectedCategories, setSelectedCategories] = useState<Array<string>>([]);
   const [selectedApp, setSelectedApp] = useState<MarketplaceApp>();
 
@@ -37,6 +37,8 @@ const Marketplace: FunctionComponent<{ onSubmit: () => void }> = ({ onSubmit })
   const {
     control,
     formState: { isValid },
+    getValues,
+    reset,
   } = useForm();
 
   const marketplaceApps = useAppSelector(({ cluster }) =>
@@ -70,13 +72,15 @@ const Marketplace: FunctionComponent<{ onSubmit: () => void }> = ({ onSubmit })
 
   const handleAddMarketplaceApp = async (app: MarketplaceApp) => {
     try {
+      const values = getValues();
       await dispatch(
-        installMarketplaceApp({ app, clusterName: selectedCluster?.clusterName as string }),
+        installMarketplaceApp({ app, clusterName: selectedCluster?.clusterName as string, values }),
       );
+      reset();
       open();
-      onSubmit();
     } catch (error) {
       //todo: handle error
+      console.log(error);
     }
   };
 
diff --git a/containers/provision/index.tsx b/containers/provision/index.tsx
index 7f56e88f..dccde41e 100644
--- a/containers/provision/index.tsx
+++ b/containers/provision/index.tsx
@@ -35,8 +35,15 @@ const Provision: FunctionComponent<ProvisionProps> = ({
   useTelemetry,
 }) => {
   const dispatch = useAppDispatch();
-  const { installType, gitProvider, installationStep, values, error } = useAppSelector(
-    ({ installation }) => installation,
+  const { installType, gitProvider, installationStep, values, error, authErrors } = useAppSelector(
+    ({ git, installation }) => ({
+      installType: installation.installType,
+      gitProvider: installation.gitProvider,
+      installationStep: installation.installationStep,
+      values: installation.values,
+      error: installation.error,
+      authErrors: git.errors,
+    }),
   );
 
   const { isProvisioned } = useAppSelector(({ api }) => api);
@@ -137,16 +144,16 @@ const Provision: FunctionComponent<ProvisionProps> = ({
     return (
       <>
         <FormContent hasInfo={hasInfo} isLastStep={isLastStep} isProvisionStep={isProvisionStep}>
-          {error && (
+          {error || authErrors.length ? (
             <ErrorContainer>
-              <ErrorBanner text={error} />
+              <ErrorBanner error={error || authErrors} />
               {isProvisionStep && (
                 <Button variant="contained" color="primary" onClick={provisionCluster}>
                   Retry
                 </Button>
               )}
             </ErrorContainer>
-          )}
+          ) : null}
           <FormFlow
             control={control}
             currentStep={installationStep}
@@ -178,6 +185,7 @@ const Provision: FunctionComponent<ProvisionProps> = ({
     isLastStep,
     isProvisionStep,
     error,
+    authErrors,
     provisionCluster,
     FormFlow,
     control,
diff --git a/containers/provision/provision.styled.ts b/containers/provision/provision.styled.ts
index b9a915f2..07a3ff60 100644
--- a/containers/provision/provision.styled.ts
+++ b/containers/provision/provision.styled.ts
@@ -28,6 +28,8 @@ export const FormContent = styled(FormContainer)<{
   background-color: ${({ isLastStep, theme }) => (isLastStep ? 'transparent' : theme.colors.white)};
   box-shadow: ${({ isProvisionStep, isLastStep }) => (isLastStep || isProvisionStep) && 'none'};
   gap: 32px;
+  height: 500px;
+  overflow: auto;
   width: 1024px;
 
   ${({ hasInfo }) =>
diff --git a/containers/service/index.tsx b/containers/service/index.tsx
index da6a41f1..ad600ef4 100644
--- a/containers/service/index.tsx
+++ b/containers/service/index.tsx
@@ -46,41 +46,41 @@ const Service: FunctionComponent<ServiceProps> = ({ links: serviceLinks, ...prop
     [dispatch, isSiteAvailable],
   );
 
-  // useEffect(() => {
-  //   if (availableSites.length) {
-  //     setLinks(
-  //       links &&
-  //         Object.keys(links).reduce(
-  //           (previous, current) => ({ ...previous, [current]: isSiteAvailable(current) }),
-  //           {},
-  //         ),
-  //     );
-  //   }
-  //   // eslint-disable-next-line react-hooks/exhaustive-deps
-  // }, [availableSites]);
+  useEffect(() => {
+    if (availableSites.length) {
+      setLinks(
+        links &&
+          Object.keys(links).reduce(
+            (previous, current) => ({ ...previous, [current]: isSiteAvailable(current) }),
+            {},
+          ),
+      );
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [availableSites]);
 
-  // useEffect(() => {
-  //   const interval = setInterval(
-  //     () =>
-  //       links &&
-  //       Object.keys(links).map((url) => {
-  //         const isAvailable = links[url];
-  //         !isAvailable && checkSiteAvailability(url);
-  //       }),
-  //     20000,
-  //   );
-  //   return () => clearInterval(interval);
-  // });
+  useEffect(() => {
+    const interval = setInterval(
+      () =>
+        links &&
+        Object.keys(links).map((url) => {
+          const isAvailable = links[url];
+          !isAvailable && checkSiteAvailability(url);
+        }),
+      20000,
+    );
+    return () => clearInterval(interval);
+  });
 
-  // useEffect(() => {
-  //   if (!firstLoad) {
-  //     setFirstLoad(true);
-  //     links &&
-  //       Object.keys(links).map(async (url) => {
-  //         await checkSiteAvailability(url);
-  //       });
-  //   }
-  // }, [checkSiteAvailability, dispatch, firstLoad, links]);
+  useEffect(() => {
+    if (!firstLoad) {
+      setFirstLoad(true);
+      links &&
+        Object.keys(links).map(async (url) => {
+          await checkSiteAvailability(url);
+        });
+    }
+  }, [checkSiteAvailability, dispatch, firstLoad, links]);
 
   return <ServiceComponent {...props} links={links} />;
 };
diff --git a/containers/services/index.tsx b/containers/services/index.tsx
index b91ed039..d989fa16 100644
--- a/containers/services/index.tsx
+++ b/containers/services/index.tsx
@@ -140,11 +140,7 @@ const Services: FunctionComponent<ServicesProps> = ({
               Learn more
             </LearnMoreLink>
           </Typography>
-          <Marketplace
-            onSubmit={() => {
-              setActiveTab(SERVICES_TABS.PROVISIONED);
-            }}
-          />
+          <Marketplace />
         </TabPanel>
       </Content>
     </Container>
diff --git a/containers/services/services.styled.ts b/containers/services/services.styled.ts
index f241d3b4..cc1ce0bb 100644
--- a/containers/services/services.styled.ts
+++ b/containers/services/services.styled.ts
@@ -2,7 +2,7 @@ import Link from 'next/link';
 import styled from 'styled-components';
 
 export const Container = styled.div`
-  height: calc(100vh - 80px);
+  height: calc(100vh - 104px);
   margin: 0 auto;
   padding-top: 40px;
   width: 1192px;
diff --git a/containers/terminalLogs/terminalLogs.tsx b/containers/terminalLogs/terminalLogs.tsx
index 06af96ed..7a73d075 100644
--- a/containers/terminalLogs/terminalLogs.tsx
+++ b/containers/terminalLogs/terminalLogs.tsx
@@ -147,14 +147,20 @@ const TerminalLogs: FunctionComponent = () => {
 
       const emitter = createLogStream(`${apiUrl}/stream`);
       emitter.on('log', (log) => {
-        const [, time] = log.message.match(/time="([^"]*)"/);
-        const [, level] = log.message.match(/level=([^"]*)/);
-        const [, msg] = log.message.match(/msg="([^"]*)"/);
-
-        const logLevel = level.replace(' msg=', '').toUpperCase();
-        const logStyle = logLevel.includes('ERROR') ? '\x1b[1;31m' : '\x1b[0;34m';
-
-        terminal.write(`\x1b[0;37m${time} ${logStyle}${logLevel}:\x1b[1;37m ${msg} \n`);
+        if (
+          log.message.includes('time=') &&
+          log.message.includes('level=') &&
+          log.message.includes('msg=')
+        ) {
+          const [, time] = log.message.match(/time="([^"]*)"/);
+          const [, level] = log.message.match(/level=([^"]*)/);
+          const [, msg] = log.message.match(/msg="([^"]*)"/);
+
+          const logLevel = level.replace(' msg=', '').toUpperCase();
+          const logStyle = logLevel.includes('ERROR') ? '\x1b[1;31m' : '\x1b[0;34m';
+
+          terminal.write(`\x1b[0;37m${time} ${logStyle}${logLevel}:\x1b[1;37m ${msg} \n`);
+        }
       });
 
       emitter.on('error', () => {
diff --git a/hooks/useInstallation.ts b/hooks/useInstallation.ts
index 96f94559..50adc5de 100644
--- a/hooks/useInstallation.ts
+++ b/hooks/useInstallation.ts
@@ -167,7 +167,7 @@ const getApiKeyInfo = (type: InstallationType) => {
       ],
     },
     [InstallationType.DIGITAL_OCEAN]: {
-      authKey: 'digitalocean_auth',
+      authKey: 'do_auth',
       fieldKeys: [
         {
           name: 'token',
diff --git a/redux/slices/git.slice.ts b/redux/slices/git.slice.ts
index e405710b..2657df16 100644
--- a/redux/slices/git.slice.ts
+++ b/redux/slices/git.slice.ts
@@ -20,11 +20,7 @@ export interface GitState {
   gitlabGroups: Array<GitLabGroup>;
   isLoading: boolean;
   isTokenValid: boolean;
-  error: string | null;
-  loadedRepositories?: boolean;
-  loadedTeams?: boolean;
-  hasExistingTeams?: boolean;
-  hasExistingRepos?: boolean;
+  errors: Array<string>;
   token?: string;
   gitOwner?: string;
 }
@@ -36,7 +32,7 @@ export const initialState: GitState = {
   gitlabGroups: [],
   isLoading: false,
   isTokenValid: false,
-  error: null,
+  errors: [],
 };
 
 const gitSlice = createSlice({
@@ -45,13 +41,12 @@ const gitSlice = createSlice({
   reducers: {
     setToken: (state, action) => {
       state.token = action.payload;
-      state.loadedRepositories = false;
-      state.loadedTeams = false;
-      state.hasExistingTeams = false;
-      state.hasExistingRepos = false;
+    },
+    setGitOwner: (state, action) => {
+      state.gitOwner = action.payload;
     },
     clearUserError: (state) => {
-      state.error = null;
+      state.errors = [];
     },
     clearGitState: (state) => {
       state.githubUser = null;
@@ -60,18 +55,8 @@ const gitSlice = createSlice({
       state.gitlabGroups = [];
       state.isLoading = false;
       state.isTokenValid = false;
-      state.error = null;
+      state.errors = [];
       state.token = undefined;
-      state.loadedRepositories = undefined;
-      state.loadedTeams = undefined;
-      state.hasExistingTeams = undefined;
-      state.hasExistingRepos = undefined;
-    },
-    clearGitValidationState: (state) => {
-      state.loadedRepositories = undefined;
-      state.loadedTeams = undefined;
-      state.hasExistingTeams = undefined;
-      state.hasExistingRepos = undefined;
     },
   },
   extraReducers: (builder) => {
@@ -81,15 +66,15 @@ const gitSlice = createSlice({
         state.githubUser = action.payload;
         state.isTokenValid = true;
       })
-      .addCase(getGithubUser.rejected, (state, action) => {
-        state.error = action.error.message ?? 'Failed to get user';
-      })
       .addCase(getGithubUserOrganizations.pending, (state) => {
         state.isLoading = true;
       })
       .addCase(getGithubUserOrganizations.rejected, (state, action) => {
         state.isLoading = false;
-        state.error = action.error.message ?? 'Failed to get users organizations';
+
+        if (action.error.message) {
+          state.errors.push('Failed to get users organizations');
+        }
       })
       .addCase(getGithubUserOrganizations.fulfilled, (state, action) => {
         state.githubUserOrganizations = action.payload.sort((a, b) =>
@@ -102,15 +87,23 @@ const gitSlice = createSlice({
         const kubefirstRepos = organizationRepos.filter(({ name }) =>
           KUBEFIRST_REPOSITORIES.includes(name),
         );
-        state.loadedRepositories = true;
-        state.hasExistingRepos = kubefirstRepos.length > 0;
+        if (kubefirstRepos.length) {
+          state.errors
+            .push(`GitHub organization <strong>${state.gitOwner}</strong> already has repositories named
+             either <strong>gitops</strong> and <strong>metaphor</strong>.
+             Please remove or rename to continue.`);
+        }
       })
       .addCase(getGitHubOrgTeams.fulfilled, (state, { payload: organizationTeams }) => {
         const kubefirstTeams = organizationTeams.filter(({ name }) =>
           KUBEFIRST_TEAMS.includes(name),
         );
-        state.loadedTeams = true;
-        state.hasExistingTeams = kubefirstTeams.length > 0;
+
+        if (kubefirstTeams.length) {
+          state.errors.push(`GitHub organization <strong> ${state.gitOwner} </strong> 
+            already has teams named <strong>admins</strong> or <strong>developers</strong>. 
+            Please remove or rename them to continue.`);
+        }
       })
       /* GitLab */
       .addCase(getGitlabUser.fulfilled, (state, action) => {
@@ -118,14 +111,18 @@ const gitSlice = createSlice({
         state.isTokenValid = true;
       })
       .addCase(getGitlabUser.rejected, (state, action) => {
-        state.error = action.error.message ?? 'Failed to get user';
+        if (action.error.message) {
+          state.errors.push('Failed to get user');
+        }
       })
       .addCase(getGitlabGroups.pending, (state) => {
         state.isLoading = true;
       })
       .addCase(getGitlabGroups.rejected, (state, action) => {
         state.isLoading = false;
-        state.error = action.error.message ?? 'Failed to get user groups';
+        if (action.error.message) {
+          state.errors.push('Failed to get user groups');
+        }
       })
       .addCase(getGitlabGroups.fulfilled, (state, action) => {
         state.gitlabGroups = action.payload.sort((a, b) => a.name.localeCompare(b.name));
@@ -141,15 +138,23 @@ const gitSlice = createSlice({
           KUBEFIRST_TEAMS.includes(name),
         );
 
-        state.loadedRepositories = true;
-        state.loadedTeams = true;
-        state.hasExistingRepos = kubefirstRepos.length > 0;
-        state.hasExistingTeams = kubefirstTeams.length > 0;
+        if (kubefirstTeams.length) {
+          state.errors
+            .push(`GitLab organization <strong>${state.gitOwner}</strong> already has teams named
+            <strong>admins</strong> or <strong>developers</strong>. 
+            Please remove or rename them to continue.`);
+        }
+
+        if (kubefirstRepos.length) {
+          state.errors
+            .push(`GitLab organization <strong>${state.gitOwner}</strong> already has repositories named
+          either <strong>gitops</strong> and <strong>metaphor</strong>.
+          Please remove or rename to continue.`);
+        }
       });
   },
 });
 
-export const { clearGitValidationState, clearGitState, clearUserError, setToken } =
-  gitSlice.actions;
+export const { clearGitState, clearUserError, setGitOwner, setToken } = gitSlice.actions;
 
 export const gitReducer = gitSlice.reducer;
diff --git a/redux/thunks/api.thunk.ts b/redux/thunks/api.thunk.ts
index a5818958..705c46a9 100644
--- a/redux/thunks/api.thunk.ts
+++ b/redux/thunks/api.thunk.ts
@@ -9,6 +9,7 @@ import {
   ClusterServices,
 } from '../../types/provision';
 import { MarketplaceApp, MarketplaceProps } from '../../types/marketplace';
+import { FieldValues } from 'react-hook-form';
 
 const mapClusterFromRaw = (cluster: ClusterResponse): Cluster => ({
   id: cluster._id,
@@ -74,8 +75,8 @@ export const createCluster = createAsyncThunk<
     civo_auth: {
       ...values?.civo_auth,
     },
-    digitalocean_auth: {
-      ...values?.digitalocean_auth,
+    do_auth: {
+      ...values?.do_auth,
     },
     vultr_auth: {
       ...values?.vultr_auth,
@@ -185,12 +186,21 @@ export const installMarketplaceApp = createAsyncThunk<
     dispatch: AppDispatch;
     state: RootState;
   }
->('api/installMarketplaceApp', async ({ app, clusterName }, { getState }) => {
+>('api/installMarketplaceApp', async ({ app, clusterName, values }, { getState }) => {
   const {
     config: { apiUrl },
   } = getState();
 
-  const res = await axios.post(`${apiUrl}/services/${clusterName}/${app.name}`);
+  const secret_keys =
+    values &&
+    Object.keys(values as FieldValues).map((key) => ({
+      name: key,
+      value: (values as FieldValues)[key],
+    }));
+
+  const res = await axios.post(`${apiUrl}/services/${clusterName}/${app.name}`, {
+    secret_keys,
+  });
 
   if ('error' in res) {
     throw res.error;
diff --git a/types/marketplace/index.ts b/types/marketplace/index.ts
index f85c0508..28718d38 100644
--- a/types/marketplace/index.ts
+++ b/types/marketplace/index.ts
@@ -1,3 +1,5 @@
+import { FieldValues } from 'react-hook-form';
+
 export interface MarketplaceApp {
   name: string;
   secret_keys?: Array<{ name: string; label: string }>;
@@ -9,4 +11,5 @@ export interface MarketplaceApp {
 export interface MarketplaceProps {
   app: MarketplaceApp;
   clusterName: string;
+  values?: FieldValues;
 }
diff --git a/types/redux/index.ts b/types/redux/index.ts
index f93511f0..60e1e54c 100644
--- a/types/redux/index.ts
+++ b/types/redux/index.ts
@@ -17,7 +17,7 @@ export interface AuthValues {
   civo_auth?: {
     token: string;
   };
-  digitalocean_auth?: {
+  do_auth?: {
     token: string;
     spaces_key: string;
     spaces_secret: string;

From 87b306c2b4c35e6a4be9812a36263a7a9bc4cb8e Mon Sep 17 00:00:00 2001
From: CristhianF7 <CristhianF7@gmail.com>
Date: Tue, 23 May 2023 18:10:04 -0500
Subject: [PATCH 6/6] fix: marketplace notification

---
 containers/marketplace/index.tsx | 19 +++++++++----------
 redux/slices/cluster.slice.ts    |  7 ++++++-
 2 files changed, 15 insertions(+), 11 deletions(-)

diff --git a/containers/marketplace/index.tsx b/containers/marketplace/index.tsx
index bddaf6c5..f69e6e9c 100644
--- a/containers/marketplace/index.tsx
+++ b/containers/marketplace/index.tsx
@@ -1,5 +1,5 @@
 import React, { FunctionComponent, useMemo, useState } from 'react';
-import { useForm, FieldValues } from 'react-hook-form';
+import { useForm } from 'react-hook-form';
 import NextLink from 'next/link';
 import intersection from 'lodash/intersection';
 import sortBy from 'lodash/sortBy';
@@ -10,9 +10,9 @@ import Typography from '../../components/typography';
 import MarketplaceCard from '../../components/marketplaceCard';
 import MarketplaceModal from '../../components/marketplaceModal';
 import useModal from '../../hooks/useModal';
-import useToggle from '../../hooks/useToggle';
 import { useAppDispatch, useAppSelector } from '../../redux/store';
 import { installMarketplaceApp } from '../../redux/thunks/api.thunk';
+import { setIsMarketplaceNotificationOpen } from '../../redux/slices/cluster.slice';
 import { MarketplaceApp } from '../../types/marketplace';
 import { VOLCANIC_SAND } from '../../constants/colors';
 
@@ -29,11 +29,12 @@ const Marketplace: FunctionComponent = () => {
   const [selectedApp, setSelectedApp] = useState<MarketplaceApp>();
 
   const dispatch = useAppDispatch();
-  const selectedCluster = useAppSelector(({ cluster }) => cluster.selectedCluster);
+  const { isMarketplaceNotificationOpen, selectedCluster } = useAppSelector(({ cluster }) => ({
+    selectedCluster: cluster.selectedCluster,
+    isMarketplaceNotificationOpen: cluster.isMarketplaceNotificationOpen,
+  }));
 
   const { isOpen, openModal, closeModal } = useModal();
-  const { isOpen: isNotificationOpen, open, close } = useToggle();
-
   const {
     control,
     formState: { isValid },
@@ -77,16 +78,14 @@ const Marketplace: FunctionComponent = () => {
         installMarketplaceApp({ app, clusterName: selectedCluster?.clusterName as string, values }),
       );
       reset();
-      open();
     } catch (error) {
       //todo: handle error
-      console.log(error);
     }
   };
 
   const handleSelectedApp = (app: MarketplaceApp) => {
+    setSelectedApp(app);
     if (app.secret_keys?.length) {
-      setSelectedApp(app);
       openModal();
     } else {
       handleAddMarketplaceApp(app);
@@ -170,9 +169,9 @@ const Marketplace: FunctionComponent = () => {
           vertical: 'bottom',
           horizontal: 'right',
         }}
-        open={isNotificationOpen}
+        open={isMarketplaceNotificationOpen}
         autoHideDuration={5000}
-        onClose={close}
+        onClose={() => dispatch(setIsMarketplaceNotificationOpen(false))}
       >
         <Alert onClose={close} severity="success" sx={{ width: '100%' }} variant="filled">
           {`${selectedApp?.name} successfully added to your cluster!`}
diff --git a/redux/slices/cluster.slice.ts b/redux/slices/cluster.slice.ts
index e0ceecb9..bee7af6c 100644
--- a/redux/slices/cluster.slice.ts
+++ b/redux/slices/cluster.slice.ts
@@ -12,12 +12,14 @@ export interface ConfigState {
   selectedCluster?: Cluster;
   clusterServices: Array<ClusterServices>;
   marketplaceApps: Array<MarketplaceApp>;
+  isMarketplaceNotificationOpen: boolean;
 }
 
 export const initialState: ConfigState = {
   selectedCluster: undefined,
   clusterServices: [],
   marketplaceApps: [],
+  isMarketplaceNotificationOpen: false,
 };
 
 const clusterSlice = createSlice({
@@ -27,6 +29,9 @@ const clusterSlice = createSlice({
     setSelectedCluster: (state, { payload: cluster }: PayloadAction<Cluster>) => {
       state.selectedCluster = cluster;
     },
+    setIsMarketplaceNotificationOpen: (state, { payload }: PayloadAction<boolean>) => {
+      state.isMarketplaceNotificationOpen = payload;
+    },
   },
   extraReducers: (builder) => {
     builder
@@ -52,6 +57,6 @@ const clusterSlice = createSlice({
   },
 });
 
-export const { setSelectedCluster } = clusterSlice.actions;
+export const { setSelectedCluster, setIsMarketplaceNotificationOpen } = clusterSlice.actions;
 
 export const clusterReducer = clusterSlice.reducer;