diff --git a/assets/frame.svg b/assets/frame.svg
new file mode 100644
index 00000000..1dd24963
--- /dev/null
+++ b/assets/frame.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/components/clusterTable/clusterTable.stories.tsx b/components/clusterTable/clusterTable.stories.tsx
index 69fda6d3..ca8c1576 100644
--- a/components/clusterTable/clusterTable.stories.tsx
+++ b/components/clusterTable/clusterTable.stories.tsx
@@ -4,6 +4,7 @@ import { Story } from '@storybook/react';
import { ClusterStatus, ClusterType } from '../../types/provision';
import { InstallationType } from '../../types/redux';
import { noop } from '../../utils/noop';
+import { sortClustersByType } from '../../utils/sortClusterByType';
import { ClusterTable, ClusterInfo } from './clusterTable';
@@ -14,7 +15,7 @@ export default {
const clusters: ClusterInfo[] = [
{
- clusterName: 'kuberfirst-mgmt2',
+ clusterName: 'kuberfirst-mgmt',
type: ClusterType.MANAGEMENT,
cloudProvider: InstallationType.AWS,
cloudRegion: 'ap-southeast-1',
@@ -27,8 +28,8 @@ const clusters: ClusterInfo[] = [
nodes: 2,
},
{
- clusterName: 'kuberfirst-mgmt2',
- type: ClusterType.MANAGEMENT,
+ clusterName: 'kuberfirst-worker-1',
+ type: ClusterType.WORKLOAD,
cloudProvider: InstallationType.CIVO,
cloudRegion: 'ap-southeast-1',
nodes: 2,
@@ -40,8 +41,8 @@ const clusters: ClusterInfo[] = [
domainName: 'yourdomain.com',
},
{
- clusterName: 'kuberfirst-mgmt2',
- type: ClusterType.MANAGEMENT,
+ clusterName: 'kuberfirst-worker-2',
+ type: ClusterType.WORKLOAD,
cloudProvider: InstallationType.DIGITAL_OCEAN,
cloudRegion: 'ap-southeast-1',
nodes: 2,
@@ -53,7 +54,7 @@ const clusters: ClusterInfo[] = [
domainName: 'yourdomain.com',
},
{
- clusterName: 'kuberfirst-mgmt2',
+ clusterName: 'kuberfirst-worker-3',
type: ClusterType.WORKLOAD,
cloudProvider: InstallationType.DIGITAL_OCEAN,
cloudRegion: 'ap-southeast-1',
@@ -66,7 +67,7 @@ const clusters: ClusterInfo[] = [
domainName: 'yourdomain.com',
},
{
- clusterName: 'kuberfirst-mgmt2',
+ clusterName: 'kuberfirst-worker-4',
type: ClusterType.WORKLOAD,
cloudProvider: InstallationType.VULTR,
cloudRegion: 'ap-southeast-1',
@@ -80,8 +81,19 @@ const clusters: ClusterInfo[] = [
},
];
-const DefaultTemplate: Story = (args) => (
-
-);
+const { managementCluster, workloadClusters } = sortClustersByType(clusters);
+
+const DefaultTemplate: Story = (args) =>
+ managementCluster ? (
+
+ ) : (
+ <>>
+ );
export const Default = DefaultTemplate.bind({});
diff --git a/components/clusterTable/clusterTable.styled.ts b/components/clusterTable/clusterTable.styled.ts
index 74484d55..f07b2d22 100644
--- a/components/clusterTable/clusterTable.styled.ts
+++ b/components/clusterTable/clusterTable.styled.ts
@@ -1,4 +1,4 @@
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
import { styled as muiStyled } from '@mui/material/styles';
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
import {
@@ -7,14 +7,21 @@ import {
tableRowClasses,
tableBodyClasses,
IconButton,
- iconButtonClasses,
typographyClasses,
Box,
+ TableContainer,
+ Table,
} from '@mui/material';
import Typography from '../typography';
import Tag from '../tag';
-import { CHEFS_HAT, PASTEL_LIGHT_BLUE, SALTBOX_BLUE, VOLCANIC_SAND } from '../../constants/colors';
+import {
+ CHEFS_HAT,
+ PASTEL_LIGHT_BLUE,
+ ROCK_BLUE,
+ SALTBOX_BLUE,
+ VOLCANIC_SAND,
+} from '../../constants/colors';
export const Menu = styled(Box)`
position: absolute;
@@ -28,12 +35,20 @@ export const Menu = styled(Box)`
z-index: 1;
`;
-export const StyledIconButton = muiStyled(IconButton)(() => ({
- [`&.${iconButtonClasses.root}`]: {
- opacity: 0,
- pointerEvents: 'none',
- },
-}));
+export const StyledIconButton = styled(IconButton)<{ expanded?: boolean }>`
+ svg {
+ color: ${ROCK_BLUE};
+ transition: transform 0.3s ease;
+ }
+
+ ${({ expanded }) =>
+ expanded &&
+ css`
+ svg {
+ transform: rotate(180deg);
+ }
+ `}
+`;
export const StyledTableBody = muiStyled(TableBody)(() => ({
[`&.${tableBodyClasses.root}`]: {
@@ -90,3 +105,15 @@ export const StyledCellText = muiStyled(Typography)(() => ({
fontWeight: 400,
},
}));
+
+export const StyledTable = styled(Table)`
+ border-collapse: collapse;
+ margin: 5px;
+ height: fit-content;
+ margin: 0 28px;
+`;
+
+export const StyledTableContainer = styled(TableContainer)`
+ display: flex;
+ height: 100%;
+`;
diff --git a/components/clusterTable/clusterTable.tsx b/components/clusterTable/clusterTable.tsx
index 9b594820..240ad12e 100644
--- a/components/clusterTable/clusterTable.tsx
+++ b/components/clusterTable/clusterTable.tsx
@@ -5,13 +5,10 @@ import React, {
useCallback,
useRef,
} from 'react';
-import Table from '@mui/material/Table';
-import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
-import { Box, Collapse, IconButton, List, ListItem, ListItemButton } from '@mui/material';
+import { IconButton, List, ListItem, ListItemButton } from '@mui/material';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
-import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import Image from 'next/image';
import moment from 'moment';
@@ -21,22 +18,25 @@ import civoLogo from '../../assets/civo_logo.svg';
import digitalOceanLogo from '../../assets/digital_ocean_logo.svg';
import vultrLogo from '../../assets/vultr_logo.svg';
import { CLUSTER_TAG_CONFIG } from '../../constants';
-import { DODGER_BLUE, FIRE_BRICK, ROCK_BLUE } from '../../constants/colors';
+import { DODGER_BLUE, FIRE_BRICK } from '../../constants/colors';
import { Cluster, ClusterStatus, ClusterType } from '../../types/provision';
import { InstallationType } from '../../types/redux';
import Typography from '../../components/typography';
import { useOnClickOutside } from '../../hooks/useOnClickOutside';
+import { noop } from '../../utils/noop';
import {
StyledTableRow,
StyledTableCell,
StyledTag,
StyledTableBody,
- StyledIconButton,
StyledTableHeading,
StyledCellText,
Menu,
StyledHeaderCell,
+ StyledIconButton,
+ StyledTableContainer,
+ StyledTable,
} from './clusterTable.styled';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -67,11 +67,15 @@ export type ClusterInfo = Pick<
};
type ClusterRowProps = ClusterInfo & {
- onMenuOpenClose: (clusterName?: string) => void;
+ expanded?: boolean;
+ onExpanseClick?: () => void;
+ onMenuOpenClose: (selectedCluster?: ClusterInfo) => void;
onDeleteCluster: () => void;
};
const ClusterRow: FunctionComponent = ({
+ expanded,
+ onExpanseClick = noop,
onDeleteCluster,
onMenuOpenClose,
...rest
@@ -87,7 +91,6 @@ const ClusterRow: FunctionComponent = ({
nodes,
} = rest;
- const [open, setOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const cloudLogoSrc = CLOUD_LOGO_OPTIONS[cloudProvider];
@@ -99,8 +102,8 @@ const ClusterRow: FunctionComponent = ({
const handleMenu = useCallback(() => {
setMenuOpen(!menuOpen);
- onMenuOpenClose(!menuOpen ? clusterName : undefined);
- }, [menuOpen, onMenuOpenClose, clusterName]);
+ onMenuOpenClose(!menuOpen ? rest : undefined);
+ }, [menuOpen, onMenuOpenClose, rest]);
const buttonRef = useRef(null);
@@ -108,19 +111,18 @@ const ClusterRow: FunctionComponent = ({
return (
<>
-
+
- setOpen(!open)}>
- {open ? (
-
- ) : (
-
- )}
-
+ {type === ClusterType.MANAGEMENT && (
+
+
+
+ )}
@@ -141,7 +143,7 @@ const ClusterRow: FunctionComponent = ({
- {moment(new Date(creationDate as string)).format('DD MMM YYYY')}
+ {moment(new Date(creationDate)).format('DD MMM YYYY')}
@@ -174,73 +176,76 @@ const ClusterRow: FunctionComponent = ({
)}
- {open && (
-
-
-
- TBD
-
-
-
- )}
>
);
};
interface ClusterTableProps extends ComponentPropsWithoutRef<'div'> {
- clusters: ClusterInfo[];
+ managementCluster: ClusterInfo;
+ workloadClusters: ClusterInfo[];
onDeleteCluster: () => void;
- onMenuOpenClose: (clusterName?: string) => void;
+ onMenuOpenClose: (selectedCluster?: ClusterInfo) => void;
}
export const ClusterTable: FunctionComponent = ({
- clusters,
+ managementCluster,
+ workloadClusters,
onDeleteCluster,
onMenuOpenClose,
...rest
-}) => (
-
-
-
-
-
-
- Name
-
-
- Cloud
-
-
- Region
-
-
- Nodes
-
-
- Created
-
-
- Created by
-
-
- Status
-
-
-
-
-
- {clusters.map((cluster) => (
+}) => {
+ const [expanded, setExpanded] = useState(false);
+
+ return (
+
+
+
+
+
+
+ Name
+
+
+ Cloud
+
+
+ Region
+
+
+ Nodes
+
+
+ Created
+
+
+ Created by
+
+
+ Status
+
+
+
+
+
setExpanded(!expanded)}
/>
- ))}
-
-
-
-);
+
+ {expanded &&
+ workloadClusters.map((cluster) => (
+
+ ))}
+
+
+
+ );
+};
diff --git a/containers/clusterManagement/index.tsx b/containers/clusterManagement/index.tsx
index 11b88fa8..0394e998 100644
--- a/containers/clusterManagement/index.tsx
+++ b/containers/clusterManagement/index.tsx
@@ -1,4 +1,4 @@
-import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';
+import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Snackbar, Tabs } from '@mui/material';
import { useRouter } from 'next/router';
@@ -15,7 +15,8 @@ import DeleteCluster from '../deleteCluster';
import TabPanel, { Tab, a11yProps } from '../../components/tab';
import { BISCAY, SALTBOX_BLUE } from '../../constants/colors';
import { Flow } from '../../components/flow';
-import { ClusterTable } from '../../components/clusterTable/clusterTable';
+import { ClusterInfo, ClusterTable } from '../../components/clusterTable/clusterTable';
+import { sortClustersByType } from '../../utils/sortClusterByType';
import { CreateClusterFlow } from './createClusterFlow';
import { Container, Content, Header } from './clusterManagement.styled';
@@ -27,7 +28,7 @@ enum MANAGEMENT_TABS {
const ClusterManagement: FunctionComponent = () => {
const [activeTab, setActiveTab] = useState(MANAGEMENT_TABS.LIST_VIEW);
- const [selectedClusterName, setSelectedClusterName] = useState();
+ const [selectedCluster, setSelectedCluster] = useState();
const isClusterZero = useAppSelector(({ config }) => config.isClusterZero);
const {
@@ -46,19 +47,21 @@ const ClusterManagement: FunctionComponent = () => {
const dispatch = useAppDispatch();
- const { isDeleted, isDeleting, isError, clusters } = useAppSelector(({ api }) => api);
+ const { isDeleted, isDeleting, isError, managementCluster, workloadClusters } = useAppSelector(
+ ({ api }) => api,
+ );
const handleGetClusters = useCallback(async (): Promise => {
await dispatch(getClusters());
}, [dispatch]);
const handleDeleteCluster = useCallback(async () => {
- if (selectedClusterName) {
- await dispatch(deleteCluster({ clusterName: selectedClusterName })).unwrap();
+ if (selectedCluster) {
+ await dispatch(deleteCluster({ clusterName: selectedCluster.clusterName })).unwrap();
handleGetClusters();
closeDeleteModal();
}
- }, [dispatch, selectedClusterName, handleGetClusters, closeDeleteModal]);
+ }, [dispatch, selectedCluster, handleGetClusters, closeDeleteModal]);
const handleCreateCluster = () => {
dispatch(resetInstallState());
@@ -72,9 +75,9 @@ const ClusterManagement: FunctionComponent = () => {
};
useEffect(() => {
- if (isDeleting && !isDeleted && selectedClusterName) {
+ if (isDeleting && !isDeleted && selectedCluster) {
interval.current = getClusterInterval({
- clusterName: selectedClusterName,
+ clusterName: selectedCluster.clusterName,
});
handleGetClusters();
}
@@ -131,11 +134,14 @@ const ClusterManagement: FunctionComponent = () => {
- setSelectedClusterName(clusterName)}
- />
+ {managementCluster && (
+ setSelectedCluster(clusterInfo)}
+ />
+ )}
@@ -148,7 +154,7 @@ const ClusterManagement: FunctionComponent = () => {
}}
open={isDeleted}
autoHideDuration={3000}
- message={`Cluster ${selectedClusterName} has been deleted`}
+ message={`Cluster ${selectedCluster?.clusterName} has been deleted`}
/>
{
>
- {selectedClusterName && (
+ {selectedCluster && (
)}
diff --git a/containers/deleteCluster/deleteCluster.stories.tsx b/containers/deleteCluster/deleteCluster.stories.tsx
index 41a88a5f..0f923c40 100644
--- a/containers/deleteCluster/deleteCluster.stories.tsx
+++ b/containers/deleteCluster/deleteCluster.stories.tsx
@@ -3,25 +3,51 @@ import { Story } from '@storybook/react';
import Button from '../../components/button';
import { noop } from '../../utils/noop';
+import { ClusterStatus, ClusterType } from '../../types/provision';
+import { InstallationType } from '../../types/redux';
+import { ClusterInfo } from '../../components/clusterTable/clusterTable';
-import DeleteCluster from './';
+import DeleteCluster, { DeleteClusterProps } from './';
export default {
title: 'Components/DeleteCluster',
component: DeleteCluster,
};
-const DefaultTemplate: Story = (args) => {
- const [open, setOpen] = useState(false);
+const clusters: ClusterInfo[] = [
+ {
+ clusterName: 'kuberfirst-mgmt',
+ type: ClusterType.MANAGEMENT,
+ cloudProvider: InstallationType.AWS,
+ cloudRegion: 'ap-southeast-1',
+ creationDate: '05 Apr 2023, 12:24:56',
+ gitUser: 'Eleanor Carroll',
+ status: ClusterStatus.PROVISIONED,
+ adminEmail: 'admin@mycompany.com',
+ gitProvider: 'Github',
+ domainName: 'yourdomain.com',
+ nodes: 2,
+ },
+ {
+ clusterName: 'kuberfirst-worker-1',
+ type: ClusterType.WORKLOAD,
+ cloudProvider: InstallationType.CIVO,
+ cloudRegion: 'ap-southeast-1',
+ nodes: 2,
+ creationDate: '05 Apr 2023, 12:24:56',
+ gitUser: 'Eleanor Carroll',
+ status: ClusterStatus.ERROR,
+ adminEmail: 'admin@mycompany.com',
+ gitProvider: 'Github',
+ domainName: 'yourdomain.com',
+ },
+];
+
+const DefaultTemplate: Story = (args) => {
+ const [open, setOpen] = useState(true);
return (
<>
- setOpen(false)}
- onDelete={noop}
- {...args}
- />
+ setOpen(false)} onDelete={noop} />
@@ -29,4 +55,12 @@ const DefaultTemplate: Story = (args) => {
);
};
-export const Default = DefaultTemplate.bind({});
+export const Management = DefaultTemplate.bind({});
+Management.args = {
+ cluster: clusters[0],
+};
+
+export const Worker = DefaultTemplate.bind({});
+Worker.args = {
+ cluster: clusters[1],
+};
diff --git a/containers/deleteCluster/deleteCluster.styled.ts b/containers/deleteCluster/deleteCluster.styled.ts
index 375a939e..904c0259 100644
--- a/containers/deleteCluster/deleteCluster.styled.ts
+++ b/containers/deleteCluster/deleteCluster.styled.ts
@@ -1,9 +1,10 @@
import styled from 'styled-components';
import Column from '../../components/column';
+import NextLinkComp from '../../components/nextLink';
export const Content = styled(Column)`
- gap: 24px;
+ gap: 8px;
margin-left: 38px;
`;
@@ -19,3 +20,11 @@ export const Header = styled.div`
gap: 12px;
margin-bottom: 8px;
`;
+
+export const NextLink = styled(NextLinkComp)`
+ a {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+`;
diff --git a/containers/deleteCluster/index.tsx b/containers/deleteCluster/index.tsx
index 7608b9fe..8e062dbb 100644
--- a/containers/deleteCluster/index.tsx
+++ b/containers/deleteCluster/index.tsx
@@ -1,47 +1,70 @@
import React, { FunctionComponent, useState } from 'react';
import { Box } from '@mui/material';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
+import LaunchOutlinedIcon from '@mui/icons-material/LaunchOutlined';
import Typography from '../../components/typography';
import TextFieldWithRef from '../../components/textField';
import Modal from '../../components/modal';
import { LAUGHING_ORANGE } from '../../constants/colors';
import Button from '../../components/button';
+import { ClusterInfo } from '../../components/clusterTable/clusterTable';
+import { ClusterType } from '../../types/provision';
-import { Content, Footer, Header } from './deleteCluster.styled';
+import { Content, Footer, Header, NextLink } from './deleteCluster.styled';
export interface DeleteClusterProps {
- clusterName: string;
+ cluster: ClusterInfo;
isOpen: boolean;
onClose: () => void;
onDelete: () => void;
}
const DeleteCluster: FunctionComponent = ({
- clusterName,
+ cluster,
isOpen,
onClose,
onDelete,
}) => {
const [matchingClusterName, setMatchingClusterName] = useState('');
+ const isManagementCluster = cluster.type === ClusterType.MANAGEMENT;
+
return (
- Delete {clusterName}
+ Delete {cluster.clusterName}
-
- Are you sure you want to delete the cluster {clusterName}? This action
- cannot be undone.
-
- setMatchingClusterName(e.target.value)}
- />
+ {isManagementCluster ? (
+ <>
+ Deleting a management cluster is carried out via the CLI.
+
+ Note: deleting a management cluster will also delete all it's
+ corresponding worker clusters
+
+
+ <>
+ How to delete a management cluster
+
+ >
+
+ >
+ ) : (
+ <>
+
+ Are you sure you want to delete the cluster {cluster.clusterName}?
+ This action cannot be undone.
+
+ setMatchingClusterName(e.target.value)}
+ />
+ >
+ )}
diff --git a/containers/header/index.tsx b/containers/header/index.tsx
index 6aa96e63..dca005e5 100644
--- a/containers/header/index.tsx
+++ b/containers/header/index.tsx
@@ -17,7 +17,9 @@ const Header: FunctionComponent = () => {
const { pathname } = useRouter();
const { isEnabled } = useFeatureFlag('cluster-management');
const { clusters, selectedCluster } = useAppSelector(({ api, cluster }) => ({
- clusters: api.clusters,
+ clusters: api.managementCluster
+ ? [api.managementCluster, ...api.workloadClusters]
+ : [...api.workloadClusters],
selectedCluster: cluster.selectedCluster,
}));
diff --git a/pages/services.tsx b/pages/services.tsx
index 06979ccd..3cb7b7e9 100644
--- a/pages/services.tsx
+++ b/pages/services.tsx
@@ -15,7 +15,9 @@ const ServicesPage: FunctionComponent = ({ isClusterZero }) =
const { selectedCluster, clusters } = useAppSelector(({ cluster, api }) => ({
selectedCluster: cluster.selectedCluster,
- clusters: api.clusters,
+ clusters: api.managementCluster
+ ? [api.managementCluster, ...api.workloadClusters]
+ : [...api.workloadClusters],
}));
const hasExistingCluster = useMemo(
diff --git a/redux/slices/api.slice.ts b/redux/slices/api.slice.ts
index 786a53ef..18aeb805 100644
--- a/redux/slices/api.slice.ts
+++ b/redux/slices/api.slice.ts
@@ -8,6 +8,7 @@ import {
getCluster,
getClusters,
} from '../thunks/api.thunk';
+import { sortClustersByType } from '../../utils/sortClusterByType';
import {
Cluster,
ClusterCreationStep,
@@ -24,7 +25,8 @@ export interface ApiState {
status?: ClusterStatus;
isError: boolean;
lastErrorCondition?: string;
- clusters: Array;
+ managementCluster?: Cluster;
+ workloadClusters: Cluster[];
selectedCluster?: Cluster;
completedSteps: Array<{ label: string; order: number }>;
cloudDomains: Array;
@@ -43,7 +45,7 @@ export const initialState: ApiState = {
isDeleted: false,
status: undefined,
loading: false,
- clusters: [],
+ workloadClusters: [],
selectedCluster: undefined,
completedSteps: [],
cloudDomains: [],
@@ -116,12 +118,17 @@ const apiSlice = createSlice({
.addCase(getClusters.fulfilled, (state, { payload }: PayloadAction>) => {
state.loading = false;
state.isError = false;
- state.clusters = payload;
+
+ const { managementCluster, workloadClusters } = sortClustersByType(payload);
+
+ state.managementCluster = managementCluster;
+ state.workloadClusters = workloadClusters;
})
.addCase(getClusters.rejected, (state) => {
state.loading = false;
state.isError = true;
- state.clusters = [];
+ state.managementCluster = undefined;
+ state.workloadClusters = [];
})
.addCase(getCloudDomains.fulfilled, (state, { payload }: PayloadAction>) => {
state.cloudDomains = payload;
diff --git a/utils/sortClusterByType.ts b/utils/sortClusterByType.ts
new file mode 100644
index 00000000..d0519924
--- /dev/null
+++ b/utils/sortClusterByType.ts
@@ -0,0 +1,19 @@
+import { Cluster, ClusterType } from '../types/provision';
+
+export function sortClustersByType(clusters: Cluster[]) {
+ let managementCluster: Cluster | undefined;
+ const workloadClusters: Cluster[] = [];
+
+ for (const cluster of clusters) {
+ if (cluster.type === ClusterType.MANAGEMENT) {
+ managementCluster = cluster;
+ } else {
+ workloadClusters.push(cluster);
+ }
+ }
+
+ return {
+ managementCluster,
+ workloadClusters,
+ };
+}