From 11884715629478cbbcc9131734f82889f5c3c997 Mon Sep 17 00:00:00 2001 From: Derrick Hawkins Date: Mon, 14 Aug 2023 12:33:57 -0700 Subject: [PATCH 1/5] refactor: update component to match design. --- .../deleteCluster/deleteCluster.stories.tsx | 32 +++++++++++++++++++ .../deleteCluster/deleteCluster.styled.ts | 5 ++- containers/deleteCluster/index.tsx | 29 +++++++++++------ 3 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 containers/deleteCluster/deleteCluster.stories.tsx diff --git a/containers/deleteCluster/deleteCluster.stories.tsx b/containers/deleteCluster/deleteCluster.stories.tsx new file mode 100644 index 00000000..41a88a5f --- /dev/null +++ b/containers/deleteCluster/deleteCluster.stories.tsx @@ -0,0 +1,32 @@ +import React, { useState } from 'react'; +import { Story } from '@storybook/react'; + +import Button from '../../components/button'; +import { noop } from '../../utils/noop'; + +import DeleteCluster from './'; + +export default { + title: 'Components/DeleteCluster', + component: DeleteCluster, +}; + +const DefaultTemplate: Story = (args) => { + const [open, setOpen] = useState(false); + return ( + <> + setOpen(false)} + onDelete={noop} + {...args} + /> + + + ); +}; + +export const Default = DefaultTemplate.bind({}); diff --git a/containers/deleteCluster/deleteCluster.styled.ts b/containers/deleteCluster/deleteCluster.styled.ts index 95ec23cf..375a939e 100644 --- a/containers/deleteCluster/deleteCluster.styled.ts +++ b/containers/deleteCluster/deleteCluster.styled.ts @@ -1,6 +1,9 @@ import styled from 'styled-components'; -export const Content = styled.div` +import Column from '../../components/column'; + +export const Content = styled(Column)` + gap: 24px; margin-left: 38px; `; diff --git a/containers/deleteCluster/index.tsx b/containers/deleteCluster/index.tsx index 644e703d..7608b9fe 100644 --- a/containers/deleteCluster/index.tsx +++ b/containers/deleteCluster/index.tsx @@ -1,8 +1,9 @@ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useState } from 'react'; import { Box } from '@mui/material'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import Typography from 'components/typography'; +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'; @@ -10,7 +11,7 @@ import Button from '../../components/button'; import { Content, Footer, Header } from './deleteCluster.styled'; export interface DeleteClusterProps { - clusterName?: string; + clusterName: string; isOpen: boolean; onClose: () => void; onDelete: () => void; @@ -22,26 +23,36 @@ const DeleteCluster: FunctionComponent = ({ onClose, onDelete, }) => { + const [matchingClusterName, setMatchingClusterName] = useState(''); return ( - +
Delete {clusterName}
- Are you sure you want to delete the cluster {clusterName}? -
-
- This action cannot be undone.{' '} + Are you sure you want to delete the cluster {clusterName}? This action + cannot be undone.
+ setMatchingClusterName(e.target.value)} + />
-
From 4b9e605596ea038c57ebe3bb8112b046c403b8e2 Mon Sep 17 00:00:00 2001 From: Derrick Hawkins Date: Mon, 14 Aug 2023 12:34:44 -0700 Subject: [PATCH 2/5] add disabled styles to error button --- components/button/button.styled.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/button/button.styled.ts b/components/button/button.styled.ts index 6ee7356d..e112391b 100644 --- a/components/button/button.styled.ts +++ b/components/button/button.styled.ts @@ -46,6 +46,15 @@ export const ErrorButton = styled(Button)` &:hover { background-color: ${({ theme }) => theme.colors.fireBrick}; } + + ${({ disabled }) => + disabled && + ` + background-color: ${LIGHT_GREY} !important; + border-color: ${EXCLUSIVE_PLUM} !important; + color: ${EXCLUSIVE_PLUM} !important; + cursor: not-allowed !important; + `} `; export const InfoButton = styled(Button)` From d08600e3f6a813f972461b113d80a15e7a084603 Mon Sep 17 00:00:00 2001 From: Derrick Hawkins Date: Mon, 14 Aug 2023 12:35:12 -0700 Subject: [PATCH 3/5] chore: add useOnClickOutside hook --- hooks/useOnClickOutside.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 hooks/useOnClickOutside.ts diff --git a/hooks/useOnClickOutside.ts b/hooks/useOnClickOutside.ts new file mode 100644 index 00000000..df91e651 --- /dev/null +++ b/hooks/useOnClickOutside.ts @@ -0,0 +1,25 @@ +import { useCallback, useEffect, RefObject } from 'react'; + +export function useOnClickOutside( + ref: RefObject, + handler: () => void, +) { + const handleClickOutside = useCallback(() => { + handler(); + }, [handler]); + + useEffect(() => { + const listener = (event: MouseEvent | TouchEvent) => { + if (!ref.current || (event.target instanceof Node && ref.current.contains(event.target))) { + return; + } + handleClickOutside(); + }; + + document.addEventListener('click', listener); + + return () => { + document.removeEventListener('click', listener); + }; + }, [ref, handleClickOutside]); +} From 705704150a6b065838e7883ed837b18d0c6bd6a2 Mon Sep 17 00:00:00 2001 From: Derrick Hawkins Date: Mon, 14 Aug 2023 12:36:09 -0700 Subject: [PATCH 4/5] fix: add delete cluster functionality back to list menu --- .../clusterTable/clusterTable.styled.ts | 16 ++- components/clusterTable/clusterTable.tsx | 105 ++++++++++++++---- containers/clusterManagement/index.tsx | 63 +++++------ 3 files changed, 130 insertions(+), 54 deletions(-) diff --git a/components/clusterTable/clusterTable.styled.ts b/components/clusterTable/clusterTable.styled.ts index 5799df98..136bb46a 100644 --- a/components/clusterTable/clusterTable.styled.ts +++ b/components/clusterTable/clusterTable.styled.ts @@ -9,11 +9,24 @@ import { IconButton, iconButtonClasses, typographyClasses, + Box, } from '@mui/material'; import Typography from '../typography'; import Tag from '../tag'; -import { PASTEL_LIGHT_BLUE, SALTBOX_BLUE, VOLCANIC_SAND } from '../../constants/colors'; +import { CHEFS_HAT, PASTEL_LIGHT_BLUE, SALTBOX_BLUE, VOLCANIC_SAND } from '../../constants/colors'; + +export const Menu = styled(Box)` + position: absolute; + bottom: -40px; + left: -110px; + width: 160px; + background-color: white; + border: 1px solid ${CHEFS_HAT}; + border-radius: 8px; + box-shadow: 0px 2px 4px 0px rgba(100, 116, 139, 0.25); + z-index: 1; +`; export const StyledIconButton = muiStyled(IconButton)(() => ({ [`&.${iconButtonClasses.root}`]: { @@ -41,6 +54,7 @@ export const StyledTag = styled(Tag)` export const StyledTableRow = muiStyled(TableRow)(() => ({ [`&.${tableRowClasses.root}`]: { border: 0, + height: 'fit-content', }, })); diff --git a/components/clusterTable/clusterTable.tsx b/components/clusterTable/clusterTable.tsx index e85ee02c..31130233 100644 --- a/components/clusterTable/clusterTable.tsx +++ b/components/clusterTable/clusterTable.tsx @@ -1,8 +1,14 @@ -import React, { useState, FunctionComponent, ComponentPropsWithoutRef } from 'react'; +import React, { + useState, + FunctionComponent, + ComponentPropsWithoutRef, + 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 } from '@mui/material'; +import { Box, Collapse, 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'; @@ -15,9 +21,11 @@ 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, ROCK_BLUE } from '../../constants/colors'; -import { Cluster, ClusterType } from '../../types/provision'; +import { DODGER_BLUE, FIRE_BRICK, ROCK_BLUE } 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 { StyledTableRow, @@ -27,6 +35,7 @@ import { StyledIconButton, StyledTableHeading, StyledCellText, + Menu, } from './clusterTable.styled'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -55,17 +64,29 @@ export type ClusterInfo = Pick< instanceSize?: string; }; -const ClusterRow: FunctionComponent = ({ - clusterName, - type, - cloudProvider, - cloudRegion, - creationDate = '', - gitUser: createdBy, - status, - nodes, +type ClusterRowProps = ClusterInfo & { + onMenuOpenClose: (clusterName?: string) => void; + onDeleteCluster: () => void; +}; + +const ClusterRow: FunctionComponent = ({ + onDeleteCluster, + onMenuOpenClose, + ...rest }) => { + const { + clusterName, + type, + cloudProvider, + cloudRegion, + creationDate = '', + gitUser: createdBy, + status, + nodes, + } = rest; + const [open, setOpen] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); const cloudLogoSrc = CLOUD_LOGO_OPTIONS[cloudProvider]; const { iconLabel, iconType, bgColor } = CLUSTER_TAG_CONFIG[status ?? 'draft']; @@ -74,10 +95,21 @@ const ClusterRow: FunctionComponent = ({ // placeholder for now. new field yet to be implemented const nodeCount = nodes ?? 2; + const handleMenu = useCallback(() => { + setMenuOpen(!menuOpen); + onMenuOpenClose(!menuOpen ? clusterName : undefined); + }, [menuOpen, onMenuOpenClose, clusterName]); + + const buttonRef = useRef(null); + + useOnClickOutside(buttonRef, () => setMenuOpen(false)); + return ( <> setOpen(!open)}> @@ -116,10 +148,28 @@ const ClusterRow: FunctionComponent = ({ - - + + + {menuOpen && ( + + + + + + Delete cluster + + + + + + )} @@ -135,11 +185,21 @@ const ClusterRow: FunctionComponent = ({ interface ClusterTableProps extends ComponentPropsWithoutRef<'div'> { clusters: ClusterInfo[]; + onDeleteCluster: () => void; + onMenuOpenClose: (clusterName?: string) => void; } -export const ClusterTable: FunctionComponent = ({ clusters, ...rest }) => ( - - +export const ClusterTable: FunctionComponent = ({ + clusters, + onDeleteCluster, + onMenuOpenClose, + ...rest +}) => ( + +
@@ -169,7 +229,12 @@ export const ClusterTable: FunctionComponent = ({ clusters, . {clusters.map((cluster) => ( - + ))}
diff --git a/containers/clusterManagement/index.tsx b/containers/clusterManagement/index.tsx index ad75fb49..11b88fa8 100644 --- a/containers/clusterManagement/index.tsx +++ b/containers/clusterManagement/index.tsx @@ -4,14 +4,12 @@ import { useRouter } from 'next/router'; import Button from '../../components/button'; import Typography from '../../components/typography'; -import { DELETE_OPTION, VIEW_DETAILS_OPTION } from '../../constants/cluster'; import { useAppDispatch, useAppSelector } from '../../redux/store'; import { deleteCluster, getCluster, getClusters } from '../../redux/thunks/api.thunk'; import { resetInstallState } from '../../redux/slices/installation.slice'; -import { Cluster, ClusterRequestProps } from '../../types/provision'; +import { ClusterRequestProps } from '../../types/provision'; import useToggle from '../../hooks/useToggle'; import Drawer from '../../components/drawer'; -import { Row } from '../../types'; import useModal from '../../hooks/useModal'; import DeleteCluster from '../deleteCluster'; import TabPanel, { Tab, a11yProps } from '../../components/tab'; @@ -29,7 +27,9 @@ enum MANAGEMENT_TABS { const ClusterManagement: FunctionComponent = () => { const [activeTab, setActiveTab] = useState(MANAGEMENT_TABS.LIST_VIEW); - const [selectedCluster, setSelectedCluster] = useState(); + const [selectedClusterName, setSelectedClusterName] = useState(); + const isClusterZero = useAppSelector(({ config }) => config.isClusterZero); + const { isOpen: isDetailsPanelOpen, open: openDetailsPanel, @@ -45,35 +45,26 @@ const ClusterManagement: FunctionComponent = () => { const { push } = useRouter(); const dispatch = useAppDispatch(); - const isClusterZero = useAppSelector(({ config }) => config.isClusterZero); + const { isDeleted, isDeleting, isError, clusters } = useAppSelector(({ api }) => api); - const handleMenuClick = (option: string, rowItem: Row) => { - const { clusterName } = rowItem; - setSelectedCluster(clusters.find((cluster) => cluster.clusterName === clusterName)); + const handleGetClusters = useCallback(async (): Promise => { + await dispatch(getClusters()); + }, [dispatch]); - if (option === DELETE_OPTION) { - openDeleteModal(); - } else if (option === VIEW_DETAILS_OPTION) { - openDetailsPanel(); + const handleDeleteCluster = useCallback(async () => { + if (selectedClusterName) { + await dispatch(deleteCluster({ clusterName: selectedClusterName })).unwrap(); + handleGetClusters(); + closeDeleteModal(); } - }; - - const handleDeleteCluster = async () => { - await dispatch(deleteCluster({ clusterName: selectedCluster?.clusterName })).unwrap(); - handleGetClusters(); - closeDeleteModal(); - }; + }, [dispatch, selectedClusterName, handleGetClusters, closeDeleteModal]); const handleCreateCluster = () => { dispatch(resetInstallState()); push('/provision'); }; - const handleGetClusters = useCallback(async (): Promise => { - await dispatch(getClusters()); - }, [dispatch]); - const getClusterInterval = (params: ClusterRequestProps) => { return setInterval(async () => { dispatch(getCluster(params)).unwrap(); @@ -81,9 +72,9 @@ const ClusterManagement: FunctionComponent = () => { }; useEffect(() => { - if (isDeleting && !isDeleted && selectedCluster) { + if (isDeleting && !isDeleted && selectedClusterName) { interval.current = getClusterInterval({ - clusterName: selectedCluster?.clusterName as string, + clusterName: selectedClusterName, }); handleGetClusters(); } @@ -140,7 +131,11 @@ const ClusterManagement: FunctionComponent = () => { - + setSelectedClusterName(clusterName)} + /> @@ -153,7 +148,7 @@ const ClusterManagement: FunctionComponent = () => { }} open={isDeleted} autoHideDuration={3000} - message={`Cluster ${selectedCluster?.clusterName} has been deleted`} + message={`Cluster ${selectedClusterName} has been deleted`} /> { > - + {selectedClusterName && ( + + )} ); }; From 53fa0ca68b71a7179cbd86ba1366650ce15dff1e Mon Sep 17 00:00:00 2001 From: Derrick Hawkins Date: Mon, 14 Aug 2023 14:36:44 -0700 Subject: [PATCH 5/5] fix:add background and round corners of table cells at 4 corners --- .../clusterTable/clusterTable.stories.tsx | 5 +- .../clusterTable/clusterTable.styled.ts | 20 ++++++++ components/clusterTable/clusterTable.tsx | 49 ++++++++++--------- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/components/clusterTable/clusterTable.stories.tsx b/components/clusterTable/clusterTable.stories.tsx index 8f9805e4..69fda6d3 100644 --- a/components/clusterTable/clusterTable.stories.tsx +++ b/components/clusterTable/clusterTable.stories.tsx @@ -3,6 +3,7 @@ import { Story } from '@storybook/react'; import { ClusterStatus, ClusterType } from '../../types/provision'; import { InstallationType } from '../../types/redux'; +import { noop } from '../../utils/noop'; import { ClusterTable, ClusterInfo } from './clusterTable'; @@ -79,6 +80,8 @@ const clusters: ClusterInfo[] = [ }, ]; -const DefaultTemplate: Story = (args) => ; +const DefaultTemplate: Story = (args) => ( + +); export const Default = DefaultTemplate.bind({}); diff --git a/components/clusterTable/clusterTable.styled.ts b/components/clusterTable/clusterTable.styled.ts index 136bb46a..74484d55 100644 --- a/components/clusterTable/clusterTable.styled.ts +++ b/components/clusterTable/clusterTable.styled.ts @@ -41,9 +41,17 @@ export const StyledTableBody = muiStyled(TableBody)(() => ({ boxShadow: `0 0 0 2px ${PASTEL_LIGHT_BLUE}`, }, })); + export const StyledTableCell = muiStyled(TableCell)(() => ({ [`&.${tableCellClasses.root}`]: { border: 0, + backgroundColor: 'white', + }, +})); + +export const StyledHeaderCell = muiStyled(StyledTableCell)(() => ({ + [`&.${tableCellClasses.root}`]: { + backgroundColor: 'transparent', }, })); @@ -56,6 +64,18 @@ export const StyledTableRow = muiStyled(TableRow)(() => ({ border: 0, height: 'fit-content', }, + ['&:first-child td:first-child']: { + borderTopLeftRadius: '20px', + }, + ['&:first-child td:last-child']: { + borderTopRightRadius: '20px', + }, + ['&:last-child td:first-child']: { + borderBottomLeftRadius: '20px', + }, + ['&:last-child td:last-child']: { + borderBottomRightRadius: '20px', + }, })); export const StyledTableHeading = muiStyled(Typography)(() => ({ diff --git a/components/clusterTable/clusterTable.tsx b/components/clusterTable/clusterTable.tsx index 31130233..8360ff8f 100644 --- a/components/clusterTable/clusterTable.tsx +++ b/components/clusterTable/clusterTable.tsx @@ -36,6 +36,7 @@ import { StyledTableHeading, StyledCellText, Menu, + StyledHeaderCell, } from './clusterTable.styled'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -172,13 +173,15 @@ const ClusterRow: FunctionComponent = ({ )}
- - - - TBD - - - + {open && ( + + + + TBD + + + + )} ); }; @@ -202,29 +205,29 @@ export const ClusterTable: FunctionComponent = ({ > - - + + Name - - + + Cloud - - + + Region - - + + Nodes - - + + Created - - + + Created by - - + + Status - - + +