From d571c5fb7c2dbefba8787bf47367152a51a790ca Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 31 Mar 2023 13:57:10 -0400 Subject: [PATCH 01/18] initial work --- .../src/nodebalancers/nodebalancer-configs.ts | 12 +- packages/manager/src/App.tsx | 6 +- packages/manager/src/MainContent.tsx | 4 +- .../src/__data__/nodeBalancerActions.ts | 18 - .../nodebalancerConfigs.container.ts | 27 - .../containers/withNodeBalancers.container.ts | 34 -- .../Domains/CreateDomain/CreateDomain.tsx | 15 +- .../NodeBalancers/NodeBalancerCreate.tsx | 12 +- .../NodeBalancerDeleteDialog.tsx | 55 +++ .../NodeBalancerConfigurations.tsx | 38 +- .../NodeBalancerDetail/NodeBalancerDetail.tsx | 449 +++++------------ .../NodeBalancerSettings.tsx | 150 ++++++ .../NodeBalancerSettings.tsx | 239 --------- .../NodeBalancerSettings/index.tsx | 2 - .../NodeBalancerCreationErrors.tsx | 72 --- .../NodeBalancerSummary.tsx | 59 +-- .../NodeBalancerSummary/SummaryPanel.tsx | 257 +++++----- .../NodeBalancerSummary/TablesPanel.tsx | 18 +- .../NodeBalancerSummary/index.tsx | 2 - .../NodeBalancerDetail/context.ts | 6 - .../NodeBalancerDetail/index.tsx | 2 - .../NodeBalancers/NodeBalancerSelect.tsx | 159 +++--- .../features/NodeBalancers/NodeBalancers.tsx | 59 +-- .../NodeBalancerActionMenu.tsx | 16 +- .../NodeBalancerTableRow.tsx | 41 +- .../NodeBalancersLanding.test.tsx | 50 -- .../NodeBalancersLanding.tsx | 460 +++++------------- .../NodeBalancersLanding/index.ts | 1 - .../src/features/NodeBalancers/index.tsx | 2 - .../src/features/NodeBalancers/types.ts | 7 - .../src/features/Search/SearchLanding.tsx | 18 +- .../src/features/Search/withStoreSearch.tsx | 41 +- .../SupportTickets/SupportTicketDrawer.tsx | 14 +- .../features/TopMenu/SearchBar/SearchBar.tsx | 9 +- packages/manager/src/hooks/useEntities.ts | 12 - .../manager/src/hooks/useNodeBalancers.ts | 23 - packages/manager/src/hooks/useReduxLoad.ts | 4 +- packages/manager/src/mocks/testBrowser.ts | 4 - packages/manager/src/queries/base.ts | 2 +- packages/manager/src/queries/nodebalancers.ts | 150 +++++- packages/manager/src/queries/volumes.ts | 2 +- packages/manager/src/store/index.ts | 18 - .../nodeBalancer/nodeBalancer.actions.ts | 71 --- .../nodeBalancer/nodeBalancer.containers.ts | 60 --- .../store/nodeBalancer/nodeBalancer.events.ts | 122 ----- .../nodeBalancer/nodeBalancer.reducer.test.ts | 268 ---------- .../nodeBalancer/nodeBalancer.reducer.ts | 144 ------ .../nodeBalancer/nodeBalancer.requests.ts | 128 ----- .../nodeBalancer/nodeBalancer.selectors.ts | 19 - .../nodeBalancerConfig/configNode.actions.ts | 57 --- .../configNode.reducer.test.ts | 135 ----- .../nodeBalancerConfig/configNode.reducer.ts | 132 ----- .../nodeBalancerConfig/configNode.requests.ts | 43 -- .../nodeBalancerConfig.actions.ts | 63 --- .../nodeBalancerConfig.containers.ts | 44 -- .../nodeBalancerConfig.events.ts | 22 - .../nodeBalancerConfig.reducer.ts | 168 ------- .../nodeBalancerConfig.requests.ts | 56 --- .../store/selectors/getSearchEntities.test.ts | 58 --- .../src/store/selectors/getSearchEntities.ts | 10 - packages/manager/src/store/selectors/index.ts | 2 - .../src/utilities/getEntityByIDFromStore.ts | 4 +- 62 files changed, 968 insertions(+), 3207 deletions(-) delete mode 100644 packages/manager/src/__data__/nodeBalancerActions.ts delete mode 100644 packages/manager/src/containers/nodebalancerConfigs.container.ts delete mode 100644 packages/manager/src/containers/withNodeBalancers.container.ts create mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx create mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx delete mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings/NodeBalancerSettings.tsx delete mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings/index.tsx delete mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerCreationErrors.tsx delete mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/index.tsx delete mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancerDetail/context.ts delete mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancerDetail/index.tsx delete mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx delete mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancersLanding/index.ts delete mode 100644 packages/manager/src/features/NodeBalancers/index.tsx delete mode 100644 packages/manager/src/hooks/useNodeBalancers.ts delete mode 100644 packages/manager/src/store/nodeBalancer/nodeBalancer.actions.ts delete mode 100644 packages/manager/src/store/nodeBalancer/nodeBalancer.containers.ts delete mode 100644 packages/manager/src/store/nodeBalancer/nodeBalancer.events.ts delete mode 100644 packages/manager/src/store/nodeBalancer/nodeBalancer.reducer.test.ts delete mode 100644 packages/manager/src/store/nodeBalancer/nodeBalancer.reducer.ts delete mode 100644 packages/manager/src/store/nodeBalancer/nodeBalancer.requests.ts delete mode 100644 packages/manager/src/store/nodeBalancer/nodeBalancer.selectors.ts delete mode 100644 packages/manager/src/store/nodeBalancerConfig/configNode.actions.ts delete mode 100644 packages/manager/src/store/nodeBalancerConfig/configNode.reducer.test.ts delete mode 100644 packages/manager/src/store/nodeBalancerConfig/configNode.reducer.ts delete mode 100644 packages/manager/src/store/nodeBalancerConfig/configNode.requests.ts delete mode 100644 packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.actions.ts delete mode 100644 packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.containers.ts delete mode 100644 packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.events.ts delete mode 100644 packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.reducer.ts delete mode 100644 packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.requests.ts delete mode 100644 packages/manager/src/store/selectors/getSearchEntities.test.ts diff --git a/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts b/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts index ecac8620e7e..fea5fbe7c31 100644 --- a/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts +++ b/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts @@ -3,8 +3,8 @@ import { UpdateNodeBalancerConfigSchema, } from '@linode/validation/lib/nodebalancers.schema'; import { API_ROOT } from '../constants'; -import Request, { setData, setMethod, setURL } from '../request'; -import { ResourcePage as Page } from '../types'; +import Request, { setData, setMethod, setParams, setURL } from '../request'; +import { ResourcePage as Page, Params } from '../types'; import { CreateNodeBalancerConfig, NodeBalancerConfig, @@ -19,10 +19,14 @@ import { combineConfigNodeAddressAndPort } from './utils'; * * @param nodeBalancerId { number } The ID of the NodeBalancer to view configs for. */ -export const getNodeBalancerConfigs = (nodeBalancerId: number) => +export const getNodeBalancerConfigs = ( + nodeBalancerId: number, + params?: Params +) => Request>( setURL(`${API_ROOT}/nodebalancers/${nodeBalancerId}/configs`), - setMethod('GET') + setMethod('GET'), + setParams(params) ); /** diff --git a/packages/manager/src/App.tsx b/packages/manager/src/App.tsx index cf259796246..8760e7490ce 100644 --- a/packages/manager/src/App.tsx +++ b/packages/manager/src/App.tsx @@ -214,7 +214,7 @@ export class App extends React.Component { render() { const { hasError } = this.state; - const { linodesError, nodeBalancersError } = this.props; + const { linodesError } = this.props; if (hasError) { return ; @@ -225,7 +225,7 @@ export class App extends React.Component { * error from the API, just render nothing because the user is * about to get shot off to login */ - if (hasOauthError(linodesError, nodeBalancersError)) { + if (hasOauthError(linodesError)) { return null; } @@ -265,7 +265,6 @@ interface StateProps { isLoggedInAsCustomer: boolean; linodesLoading: boolean; linodesError?: APIError[]; - nodeBalancersError?: APIError[]; bucketsError?: APIError[]; appIsLoading: boolean; euuid?: string; @@ -282,7 +281,6 @@ const mapStateToProps: MapState = (state) => ({ state ), linodesLoading: state.__resources.linodes.loading, - nodeBalancersError: path(['read'], state.__resources.nodeBalancers.error), appIsLoading: state.initialLoad.appIsLoading, featureFlagsLoading: state.featureFlagsLoad.featureFlagsLoading, }); diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 35f87e93bc7..2ad42f9726e 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -151,7 +151,9 @@ const Images = React.lazy(() => import('src/features/Images')); const Kubernetes = React.lazy(() => import('src/features/Kubernetes')); const ObjectStorage = React.lazy(() => import('src/features/ObjectStorage')); const Profile = React.lazy(() => import('src/features/Profile')); -const NodeBalancers = React.lazy(() => import('src/features/NodeBalancers')); +const NodeBalancers = React.lazy( + () => import('src/features/NodeBalancers/NodeBalancers') +); const StackScripts = React.lazy(() => import('src/features/StackScripts')); const SupportTickets = React.lazy( () => import('src/features/Support/SupportTickets') diff --git a/packages/manager/src/__data__/nodeBalancerActions.ts b/packages/manager/src/__data__/nodeBalancerActions.ts deleted file mode 100644 index 5e0fd096352..00000000000 --- a/packages/manager/src/__data__/nodeBalancerActions.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const mockNodeBalancerActions = { - nodeBalancerActions: { - getAllNodeBalancersWithConfigs: jest.fn(), - getAllNodeBalancers: jest.fn(), - createNodeBalancer: jest.fn(), - deleteNodeBalancer: jest.fn(), - updateNodeBalancer: jest.fn(), - }, -}; - -export const modeNodeBalancerConfigActions = { - nodeBalancerConfigActions: { - getAllNodeBalancerConfigs: jest.fn(), - createNodeBalancerConfig: jest.fn(), - updateNodeBalancerConfig: jest.fn(), - deleteNodeBalancerConfig: jest.fn(), - }, -}; diff --git a/packages/manager/src/containers/nodebalancerConfigs.container.ts b/packages/manager/src/containers/nodebalancerConfigs.container.ts deleted file mode 100644 index 46041638a45..00000000000 --- a/packages/manager/src/containers/nodebalancerConfigs.container.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NodeBalancerConfig } from '@linode/api-v4/lib/nodebalancers'; -import { APIError } from '@linode/api-v4/lib/types'; -import { connect, MapStateToProps } from 'react-redux'; -import { ApplicationState } from 'src/store'; - -export interface StateProps { - configs: NodeBalancerConfig[]; - configsLoading: boolean; - configsError?: Error | APIError[]; -} - -const container = (nodeBalancerId: number) => { - const mapStateToProps: MapStateToProps = ( - state - ) => { - const thisNBConfigState = - state.__resources.nodeBalancerConfigs[nodeBalancerId]; - return { - configs: Object.values(thisNBConfigState.itemsById ?? {}), - configsLoading: thisNBConfigState.loading ?? false, - configsError: thisNBConfigState.error?.read ?? [], - }; - }; - return connect(mapStateToProps); -}; - -export default container; diff --git a/packages/manager/src/containers/withNodeBalancers.container.ts b/packages/manager/src/containers/withNodeBalancers.container.ts deleted file mode 100644 index d23031d34c8..00000000000 --- a/packages/manager/src/containers/withNodeBalancers.container.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { connect } from 'react-redux'; -import { ApplicationState } from 'src/store'; - -export default () => - connect((state: ApplicationState) => { - const { - lastUpdated, - loading, - error, - itemsById, - results, - } = state.__resources.nodeBalancers; - - /** itemsById looks like this - * - * { - * 123: NodeBalancer, - * 4234: NodeBalancer - * } - * - * we want it to look like - * - * NodeBalancer[] - */ - const nodeBalancers = Object.values(itemsById); - - return { - nodeBalancersData: nodeBalancers, - nodeBalancersLoading: loading, - nodeBalancersError: error?.read, - nodeBalancersLastUpdated: lastUpdated, - nodeBalancersResults: results, - }; - }); diff --git a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx index 24e3a02af29..b99f34a0f1d 100644 --- a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx +++ b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx @@ -28,7 +28,7 @@ import Radio from 'src/components/Radio'; import TextField from 'src/components/TextField'; import { reportException } from 'src/exceptionReporting'; import LinodeSelect from 'src/features/linodes/LinodeSelect'; -import NodeBalancerSelect from 'src/features/NodeBalancers/NodeBalancerSelect'; +import { NodeBalancerSelect } from 'src/features/NodeBalancers/NodeBalancerSelect'; import { hasGrant } from 'src/features/Profile/permissionsHelpers'; import { useCreateDomainMutation } from 'src/queries/domains'; import { useGrants, useProfile } from 'src/queries/profile'; @@ -282,9 +282,6 @@ export const CreateDomain = () => { const updateSelectedLinode = (linode: Linode) => setSelectedDefaultLinode(linode); - const updateSelectedNodeBalancer = (nodebalancer: NodeBalancer) => - setSelectedDefaultNodeBalancer(nodebalancer); - const updateInsertDefaultRecords = (value: Item) => setDefaultRecordsSetting(value); @@ -455,13 +452,11 @@ export const CreateDomain = () => { defaultRecordsSetting.value === 'nodebalancer' && ( + setSelectedDefaultNodeBalancer(nodebalancer) } + value={selectedDefaultNodeBalancer?.id} disabled={disabled} /> {!errorMap.defaultNodeBalancer && ( diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index d423308d70c..d3ed2c5c754 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -32,10 +32,6 @@ import { Tag } from 'src/components/TagsInput'; import withProfile, { ProfileProps } from 'src/components/withProfile'; import withRegions from 'src/containers/regions.container'; import { hasGrant } from 'src/features/Profile/permissionsHelpers'; -import { - withNodeBalancerActions, - WithNodeBalancerActions, -} from 'src/store/nodeBalancer/nodeBalancer.containers'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { isEURegion } from 'src/utilities/formatRegion'; import { sendCreateNodeBalancerEvent } from 'src/utilities/ga'; @@ -60,6 +56,7 @@ import { } from 'src/queries/accountAgreements'; import LandingHeader from 'src/components/LandingHeader'; import { Region } from '@linode/api-v4/lib/regions'; +import { createNodeBalancer } from '@linode/api-v4/lib/nodebalancers'; type ClassNames = 'title' | 'sidebar'; @@ -81,8 +78,7 @@ const styles = (theme: Theme) => }, }); -type CombinedProps = WithNodeBalancerActions & - ProfileProps & +type CombinedProps = ProfileProps & WithRegions & RouteComponentProps<{}> & WithStyles & @@ -305,9 +301,6 @@ class NodeBalancerCreate extends React.Component { }; createNodeBalancer = () => { - const { - nodeBalancerActions: { createNodeBalancer }, - } = this.props; const { nodeBalancerFields, signedAgreement } = this.state; /* transform node data for the requests */ @@ -854,7 +847,6 @@ interface WithRegions { export default recompose( withRegions, - withNodeBalancerActions, styled, withRouter, withProfile, diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx new file mode 100644 index 00000000000..1d5a55f3c4e --- /dev/null +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { useHistory } from 'react-router-dom'; +import Typography from 'src/components/core/Typography'; +import Notice from 'src/components/Notice'; +import TypeToConfirmDialog from 'src/components/TypeToConfirmDialog'; +import { useNodebalancerDeleteMutation } from 'src/queries/nodebalancers'; + +interface Props { + id: number; + label: string; + open: boolean; + onClose: () => void; +} + +export const NodeBalancerDeleteDialog = ({ + id, + label, + open, + onClose, +}: Props) => { + const { mutateAsync, isLoading, error } = useNodebalancerDeleteMutation(id); + const { push } = useHistory(); + + const onDelete = async () => { + await mutateAsync(); + push('/nodebalancers'); + }; + + return ( + + + + Deleting this NodeBalancer is permanent and can’t be undone. + + + + Traffic will no longer be routed through this NodeBalancer. Please check + your DNS settings and either provide the IP address of another active + NodeBalancer, or route traffic directly to your Linode. + + + ); +}; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx index a3294249a7c..fced1aa47d7 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx @@ -1,10 +1,13 @@ import { + createNodeBalancerConfig, createNodeBalancerConfigNode, + deleteNodeBalancerConfig, deleteNodeBalancerConfigNode, getNodeBalancerConfigNodes, getNodeBalancerConfigs, NodeBalancerConfig, NodeBalancerConfigNode, + updateNodeBalancerConfig, updateNodeBalancerConfigNode, } from '@linode/api-v4/lib/nodebalancers'; import { APIError, ResourcePage } from '@linode/api-v4/lib/types'; @@ -35,10 +38,6 @@ import Grid from 'src/components/Grid'; import PromiseLoader, { PromiseLoaderResponse, } from 'src/components/PromiseLoader/PromiseLoader'; -import { - withNodeBalancerConfigActions, - WithNodeBalancerConfigActions, -} from 'src/store/nodeBalancerConfig/nodeBalancerConfig.containers'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; import NodeBalancerConfigPanel from '../NodeBalancerConfigPanel'; @@ -117,7 +116,6 @@ interface State { } type CombinedProps = Props & - WithNodeBalancerConfigActions & RouteProps & WithStyles & PreloadedProps; @@ -289,7 +287,6 @@ class NodeBalancerConfigurations extends React.Component { ) => { /* Update a config and its nodes simultaneously */ const { - nodeBalancerConfigActions: { updateNodeBalancerConfig }, match: { params: { nodeBalancerId }, }, @@ -302,11 +299,11 @@ class NodeBalancerConfigurations extends React.Component { return; } - const nodeBalUpdate = updateNodeBalancerConfig({ - nodeBalancerId: Number(nodeBalancerId), - nodeBalancerConfigId: config.id, - ...configPayload, - }) + const nodeBalUpdate = updateNodeBalancerConfig( + Number(nodeBalancerId), + config.id, + configPayload + ) .then((nodeBalancerConfig) => { // update config data const newConfigs = clone(this.state.configs); @@ -413,7 +410,6 @@ class NodeBalancerConfigurations extends React.Component { */ const { - nodeBalancerConfigActions: { createNodeBalancerConfig }, match: { params: { nodeBalancerId }, }, @@ -423,10 +419,7 @@ class NodeBalancerConfigurations extends React.Component { return; } - createNodeBalancerConfig({ - nodeBalancerId: Number(nodeBalancerId), - ...configPayload, - }) + createNodeBalancerConfig(Number(nodeBalancerId), configPayload) .then((nodeBalancerConfig) => { // update config data const newConfigs = clone(this.state.configs); @@ -579,7 +572,6 @@ class NodeBalancerConfigurations extends React.Component { }); const { - nodeBalancerConfigActions: { deleteNodeBalancerConfig }, match: { params: { nodeBalancerId }, }, @@ -593,10 +585,7 @@ class NodeBalancerConfigurations extends React.Component { } // actually delete a real config - deleteNodeBalancerConfig({ - nodeBalancerId: Number(nodeBalancerId), - nodeBalancerConfigId: config.id, - }) + deleteNodeBalancerConfig(Number(nodeBalancerId), config.id) .then((_) => { // update config data const newConfigs = clone(this.state.configs); @@ -1162,11 +1151,6 @@ const preloaded = PromiseLoader({ }, }); -const enhanced = composeC( - styled, - withRouter, - preloaded, - withNodeBalancerConfigActions -); +const enhanced = composeC(styled, withRouter, preloaded); export default enhanced(NodeBalancerConfigurations); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx index 0a5e3098d4a..52d8112fc04 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx @@ -1,362 +1,155 @@ -import { - getNodeBalancer, - getNodeBalancerConfigs, - NodeBalancer, -} from '@linode/api-v4/lib/nodebalancers'; -import { APIError } from '@linode/api-v4/lib/types'; -import { withSnackbar, WithSnackbarProps } from 'notistack'; -import { last, pathOr } from 'ramda'; import * as React from 'react'; -import { matchPath, RouteComponentProps } from 'react-router-dom'; -import { compose } from 'recompose'; +import Breadcrumb from 'src/components/Breadcrumb'; import CircleProgress from 'src/components/CircleProgress'; import TabPanels from 'src/components/core/ReachTabPanels'; import Tabs from 'src/components/core/ReachTabs'; +import DocsLink from 'src/components/DocsLink'; import ErrorState from 'src/components/ErrorState'; +import Grid from 'src/components/Grid'; import Notice from 'src/components/Notice'; import SafeTabPanel from 'src/components/SafeTabPanel'; import TabLinkList from 'src/components/TabLinkList'; -import withLoadingAndError, { - LoadingAndErrorHandlers, - LoadingAndErrorState, -} from 'src/components/withLoadingAndError'; -import withFeatureFlagConsumerContainer, { - FeatureFlagConsumerProps, -} from 'src/containers/withFeatureFlagConsumer.container'; -import { - withNodeBalancerActions, - WithNodeBalancerActions, -} from 'src/store/nodeBalancer/nodeBalancer.containers'; -import { - getAPIErrorOrDefault, - getErrorMap, - getErrorStringOrDefault, -} from 'src/utilities/errorUtils'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; -import { ExtendedNodeBalancer } from '../types'; -import { NodeBalancerProvider } from './context'; import NodeBalancerConfigurations from './NodeBalancerConfigurations'; import NodeBalancerSettings from './NodeBalancerSettings'; -import NodeBalancerSummary from './NodeBalancerSummary'; -import LandingHeader from 'src/components/LandingHeader'; -import { sendEvent } from 'src/utilities/ga'; - -type RouteProps = RouteComponentProps<{ nodeBalancerId?: string }>; - -interface State { - nodeBalancer?: ExtendedNodeBalancer; - ApiError: APIError[] | undefined; - labelInput?: string; -} - -type CombinedProps = WithNodeBalancerActions & - WithSnackbarProps & - RouteProps & - FeatureFlagConsumerProps & - LoadingAndErrorHandlers & - LoadingAndErrorState; - -class NodeBalancerDetail extends React.Component { - state: State = { - nodeBalancer: undefined, - ApiError: undefined, - labelInput: undefined, - }; - - pollInterval: number; - - requestNodeBalancer = (nodeBalancerId: number) => - Promise.all([ - getNodeBalancer(+nodeBalancerId), - getNodeBalancerConfigs(+nodeBalancerId), - ]) - .then(([nodeBalancer, configsData]) => { - const { data: configs } = configsData; - return { - ...nodeBalancer, - down: configs.reduce((acc: number, config) => { - return acc + config.nodes_status.down; - }, 0), // add the downtime for each config together - up: configs.reduce((acc: number, config) => { - return acc + config.nodes_status.up; - }, 0), // add the uptime for each config together - configPorts: configs.reduce((acc: [number], config) => { - return [...acc, { configId: config.id, port: config.port }]; - }, []), - }; - }) - .then((nodeBalancer: ExtendedNodeBalancer) => { - this.setState({ nodeBalancer }); - this.props.clearLoadingAndErrors(); - }) - .catch((error) => { - if (!this.state.nodeBalancer) { - this.props.setErrorAndClearLoading( - getErrorStringOrDefault( - error, - 'There was an error loading your NodeBalancer.' - ) - ); - } - }); - - componentDidMount() { - const { nodeBalancerId } = this.props.match.params; - - // On initial load only, we want a loading state. - this.props.setLoadingAndClearErrors(); - if (!nodeBalancerId) { - this.props.setErrorAndClearLoading('NodeBalancer ID param not set.'); - return; - } - this.requestNodeBalancer(+nodeBalancerId); - - // Update NB information every 30 seconds, so that we have an accurate picture of nodes - this.pollInterval = window.setInterval(() => { - if (document.visibilityState === 'visible') { - this.requestNodeBalancer(+nodeBalancerId); - } - }, 30 * 1000); - } - - componentWillUnmount() { - clearInterval(this.pollInterval); - } - - updateLabel = (label: string) => { - const { nodeBalancer } = this.state; - const { - nodeBalancerActions: { updateNodeBalancer }, - } = this.props; - - // This should never actually happen, but TypeScript is expecting a Promise here. - if (nodeBalancer === undefined) { - return Promise.resolve(); - } - - this.setState({ ApiError: undefined }); - - return updateNodeBalancer({ nodeBalancerId: nodeBalancer.id, label }) - .then(() => { - this.setState({ - nodeBalancer: { ...nodeBalancer, label }, - labelInput: label, - }); - }) - .catch((error) => { - this.setState( - () => ({ - ApiError: getAPIErrorOrDefault( - error, - 'Error updating label', - 'label' - ), - labelInput: label, - }), - () => { - scrollErrorIntoView(); - } - ); - return Promise.reject(error); - }); - }; - - updateTags = (tags: string[]) => { - const { nodeBalancer } = this.state; - const { - nodeBalancerActions: { updateNodeBalancer }, - } = this.props; - - if (nodeBalancer === undefined) { - return; +import NodeBalancerSummary from './NodeBalancerSummary/NodeBalancerSummary'; +import { getErrorMap } from 'src/utilities/errorUtils'; +import { makeStyles } from 'tss-react/mui'; +import { Theme } from '@mui/material/styles'; +import { + useNodeBalancerQuery, + useNodebalancerUpdateMutation, +} from 'src/queries/nodebalancers'; +import { + matchPath, + useHistory, + useLocation, + useParams, +} from 'react-router-dom'; + +const useStyles = makeStyles()((theme: Theme) => ({ + root: { + [theme.breakpoints.down('sm')]: { + paddingLeft: theme.spacing(), + }, + }, +})); + +const NodeBalancerDetail = () => { + const { classes } = useStyles(); + const history = useHistory(); + const location = useLocation(); + const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); + const id = Number(nodeBalancerId); + const [label, setLabel] = React.useState(); + + const { + mutateAsync: updateNodeBalancer, + error: updateError, + } = useNodebalancerUpdateMutation(id); + + const { data: nodebalancer, isLoading, error } = useNodeBalancerQuery(id); + + React.useEffect(() => { + if (label !== nodebalancer?.label) { + setLabel(nodebalancer?.label); } + }, [nodebalancer]); - return updateNodeBalancer({ nodeBalancerId: nodeBalancer.id, tags }) - .then(() => { - this.setState({ - nodeBalancer: { ...nodeBalancer, tags }, - ApiError: undefined, - }); - }) - .catch((error) => { - const ApiError = getAPIErrorOrDefault(error, 'Error creating tag'); - this.setState({ - ApiError, - }); - return Promise.reject(ApiError); - }); - }; - - cancelUpdate = () => { - this.setState({ - ApiError: undefined, - labelInput: pathOr('', ['label'], this.state.nodeBalancer), - }); + const cancelUpdate = () => { + setLabel(nodebalancer?.label); }; - getLabelLink = (): string | undefined => { - return last(location.pathname.split('/')) !== 'summary' - ? 'summary' - : undefined; - }; - - static docs: Linode.Doc[] = [ - { - title: 'Getting Started with NodeBalancers', - src: - 'https://www.linode.com/docs/platform/nodebalancer/getting-started-with-nodebalancers-new-manager/', - body: `Using a NodeBalancer to begin managing a simple web application`, - }, - { - title: 'NodeBalancer Reference Guide', - src: - 'https://www.linode.com/docs/platform/nodebalancer/nodebalancer-reference-guide-new-manager/', - body: `NodeBalancer Reference Guide`, - }, - ]; - - tabs = [ + const tabs = [ { - routeName: `${this.props.match.url}/summary`, + routeName: `/nodebalancers/${id}/summary`, title: 'Summary', }, { - routeName: `${this.props.match.url}/configurations`, + routeName: `/nodebalancers/${id}/configurations`, title: 'Configurations', }, { - routeName: `${this.props.match.url}/settings`, + routeName: `/nodebalancers/${id}/settings`, title: 'Settings', }, ]; - updateNodeBalancerState = (data: NodeBalancer) => { - if (this.state.nodeBalancer) { - this.setState({ - nodeBalancer: { - ...this.state.nodeBalancer, - label: data.label, - client_conn_throttle: data.client_conn_throttle, - }, - labelInput: data.label, - }); - } - }; + const matches = (pathName: string) => + Boolean(matchPath(location.pathname, { path: pathName })); - render() { - const matches = (pathName: string) => - Boolean(matchPath(this.props.location.pathname, { path: pathName })); - const { error, loading } = this.props; - const { nodeBalancer } = this.state; - - /** Loading State */ - if (loading) { - return ; - } - - /** Error State */ - if (error) { - return ( - - ); - } + if (isLoading) { + return ; + } - /** Empty State */ - if (!nodeBalancer) { - return null; - } + if (error) { + return ( + + ); + } - const errorMap = getErrorMap(['label'], this.state.ApiError); - const labelError = errorMap.label; + if (!nodebalancer) { + return null; + } - const nodeBalancerLabel = - this.state.labelInput !== undefined - ? this.state.labelInput - : nodeBalancer.label; + const errorMap = getErrorMap(['label'], updateError); + const labelError = errorMap.label; - const p = { - updateTags: this.updateTags, - }; + const nodeBalancerLabel = label !== undefined ? label : nodebalancer?.label; - const navToURL = (index: number) => { - this.props.history.push(this.tabs[index].routeName); - }; + const navToURL = (index: number) => { + history.push(tabs[index].routeName); + }; - return ( - - - { - sendEvent({ - category: 'Node Balancer Details', - action: 'Click:link', - label: 'Getting Started', - }); + return ( + + + + updateNodeBalancer({ label }), + onCancel: cancelUpdate, + errorText: labelError, }} /> - {errorMap.none && } - matches(tab.routeName)), - 0 - )} - onChange={navToURL} - > - - - - - - - - - - - - - - - - - - - ); - } -} - -export default compose( - withFeatureFlagConsumerContainer, - withSnackbar, - withNodeBalancerActions, - withLoadingAndError -)(NodeBalancerDetail); + + + + + + {errorMap.none && } + matches(tab.routeName)), + 0 + )} + onChange={navToURL} + > + + + + + + + + + + + + + + + + ); +}; + +export default NodeBalancerDetail; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx new file mode 100644 index 00000000000..99d10be3ed0 --- /dev/null +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx @@ -0,0 +1,150 @@ +import * as React from 'react'; +import Accordion from 'src/components/Accordion'; +import Button from 'src/components/Button'; +import FormHelperText from 'src/components/core/FormHelperText'; +import InputAdornment from 'src/components/core/InputAdornment'; +import TextField from 'src/components/TextField'; +import defaultNumeric from 'src/utilities/defaultNumeric'; +import { useParams } from 'react-router-dom'; +import { clamp, compose, defaultTo } from 'ramda'; +import { Theme } from '@mui/material/styles'; +import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; +import { makeStyles } from 'tss-react/mui'; +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { + useNodeBalancerQuery, + useNodebalancerUpdateMutation, +} from 'src/queries/nodebalancers'; + +const useStyles = makeStyles()((theme: Theme) => ({ + spacing: { + marginTop: theme.spacing(), + }, +})); + +export const NodeBalancerSettings = () => { + const { classes } = useStyles(); + const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); + const id = Number(nodeBalancerId); + + const { data: nodebalancer } = useNodeBalancerQuery(id); + + const { + mutateAsync: updateNodeBalancerLabel, + error: labelError, + isLoading: isUpdatingLabel, + } = useNodebalancerUpdateMutation(id); + + const { + mutateAsync: updateNodeBalancerThrottle, + error: throttleError, + isLoading: isUpdatingThrottle, + } = useNodebalancerUpdateMutation(id); + + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState( + false + ); + + const [label, setLabel] = React.useState(nodebalancer?.label); + + const [connectionThrottle, setConnectionThrottle] = React.useState( + nodebalancer?.client_conn_throttle + ); + + React.useEffect(() => { + if (label !== nodebalancer?.label) { + setLabel(nodebalancer?.label); + } + if (connectionThrottle !== nodebalancer?.client_conn_throttle) { + setConnectionThrottle(nodebalancer?.client_conn_throttle); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nodebalancer]); + + if (!nodebalancer) { + return null; + } + + return ( +
+ + + setLabel(e.target.value)} + value={label} + data-qa-label-panel + /> + + + + / second + ), + }} + label="Connection Throttle" + errorText={throttleError?.[0].reason} + onChange={(e) => + setConnectionThrottle( + controlClientConnectionThrottle(e.target.value) + ) + } + placeholder="0" + value={defaultTo(0, connectionThrottle)} + data-qa-connection-throttle + /> + + To help mitigate abuse, throttle connections from a single client IP + to this number per second. 0 to disable. + + + + + + + setIsDeleteDialogOpen(false)} + /> +
+ ); +}; + +const controlClientConnectionThrottle = compose(clamp(0, 20), (value: string) => + defaultNumeric(0, value) +); + +export default NodeBalancerSettings; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings/NodeBalancerSettings.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings/NodeBalancerSettings.tsx deleted file mode 100644 index 3f4898a6208..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings/NodeBalancerSettings.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; -import { APIError } from '@linode/api-v4/lib/types'; -import { clamp, compose, defaultTo } from 'ramda'; -import * as React from 'react'; -import { useHistory } from 'react-router-dom'; -import { compose as composeC } from 'recompose'; -import Accordion from 'src/components/Accordion'; -import Button from 'src/components/Button'; -import FormHelperText from 'src/components/core/FormHelperText'; -import InputAdornment from 'src/components/core/InputAdornment'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import Typography from 'src/components/core/Typography'; -import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import Notice from 'src/components/Notice'; -import TextField from 'src/components/TextField'; -import TypeToConfirmDialog from 'src/components/TypeToConfirmDialog'; -import { - withNodeBalancerActions, - WithNodeBalancerActions, -} from 'src/store/nodeBalancer/nodeBalancer.containers'; -import defaultNumeric from 'src/utilities/defaultNumeric'; -import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; - -const useStyles = makeStyles((theme: Theme) => ({ - spacing: { - marginTop: theme.spacing(), - }, -})); - -interface Props { - nodeBalancerId: number; - nodeBalancerLabel: string; - nodeBalancerClientConnThrottle: number; - updateNodeBalancerDetailState: (data: NodeBalancer) => void; -} - -type CombinedProps = Props & WithNodeBalancerActions; - -export const NodeBalancerSettings: React.FC = (props) => { - const classes = useStyles(); - const history = useHistory(); - - const { - nodeBalancerId, - nodeBalancerLabel, - nodeBalancerClientConnThrottle, - nodeBalancerActions: { updateNodeBalancer, deleteNodeBalancer }, - updateNodeBalancerDetailState, - } = props; - - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState( - false - ); - const [isDeleting, setIsDeleting] = React.useState(false); - const [deleteError, setDeleteError] = React.useState( - undefined - ); - - const [label, setLabel] = React.useState(props.nodeBalancerLabel); - const [isLabelSaving, setIsLabelSaving] = React.useState(false); - const [labelError, setLabelError] = React.useState( - undefined - ); - - const [connectionThrottle, setConnectionThrottle] = React.useState( - nodeBalancerClientConnThrottle - ); - const [ - isConnectionThrottleSaving, - setIsConnectionThrottleSaving, - ] = React.useState(false); - const [connectionThrottleError, setConnectionThrottleError] = React.useState< - string | undefined - >(undefined); - - React.useEffect(() => { - if (label !== props.nodeBalancerLabel) { - setLabel(props.nodeBalancerLabel); - } - if (connectionThrottle !== props.nodeBalancerClientConnThrottle) { - setConnectionThrottle(props.nodeBalancerClientConnThrottle); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props]); - - const onSaveLabel = () => { - setIsLabelSaving(true); - setLabelError(undefined); - - updateNodeBalancer({ nodeBalancerId, label }) - .then((data) => { - setIsLabelSaving(false); - updateNodeBalancerDetailState(data); - }) - .catch((error) => { - setIsLabelSaving(false); - setLabelError( - getAPIErrorOrDefault( - error, - 'Unable to update your NodeBalancer\u{2019}s label.' - )[0].reason - ); - }); - }; - - const onSaveConnectionThrottle = () => { - setIsConnectionThrottleSaving(true); - setConnectionThrottleError(undefined); - - updateNodeBalancer({ - nodeBalancerId, - client_conn_throttle: connectionThrottle, - }) - .then((data) => { - setIsConnectionThrottleSaving(false); - updateNodeBalancerDetailState(data); - }) - .catch((error) => { - setIsConnectionThrottleSaving(false); - setLabelError( - getAPIErrorOrDefault( - error, - 'Unable to update your NodeBalancer\u{2019}s client connection throttle.' - )[0].reason - ); - }); - }; - - const handleDelete = () => { - setIsDeleting(true); - setDeleteError(undefined); - - deleteNodeBalancer({ nodeBalancerId }) - .then(() => { - history.replace('/nodebalancers'); - }) - .catch((errors: APIError[]) => { - setDeleteError(errors); - setIsDeleting(false); - }); - }; - - return ( -
- - - setLabel(e.target.value)} - value={label} - data-qa-label-panel - /> - - - - / second - ), - }} - label="Connection Throttle" - errorText={connectionThrottleError} - onChange={(e) => - setConnectionThrottle( - controlClientConnectionThrottle(e.target.value) - ) - } - placeholder="0" - value={defaultTo(0, connectionThrottle)} - data-qa-connection-throttle - /> - - To help mitigate abuse, throttle connections from a single client IP - to this number per second. 0 to disable. - - - - - - - setIsDeleteDialogOpen(false)} - onClick={handleDelete} - typographyStyle={{ marginTop: '20px' }} - > - - - Deleting this NodeBalancer is permanent and can’t be undone. - - - - Traffic will no longer be routed through this NodeBalancer. Please - check your DNS settings and either provide the IP address of another - active NodeBalancer, or route traffic directly to your Linode. - - -
- ); -}; - -const controlClientConnectionThrottle = compose(clamp(0, 20), (value: string) => - defaultNumeric(0, value) -); - -const enhanced = composeC(withNodeBalancerActions); - -export default enhanced(NodeBalancerSettings); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings/index.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings/index.tsx deleted file mode 100644 index bf27384dcee..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import NodeBalancerSettings from './NodeBalancerSettings'; -export default NodeBalancerSettings; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerCreationErrors.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerCreationErrors.tsx deleted file mode 100644 index 2faab64311f..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerCreationErrors.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { - NodeBalancerConfig, - NodeBalancerConfigNode, -} from '@linode/api-v4/lib/nodebalancers'; -import { APIError } from '@linode/api-v4/lib/types'; -import * as React from 'react'; -import List from 'src/components/core/List'; -import ListItem from 'src/components/core/ListItem'; -import ListItemText from 'src/components/core/ListItemText'; -import Notice from 'src/components/Notice'; - -interface ErrorResponse { - errors: APIError[]; -} - -interface ConfigErrorResponse extends ErrorResponse { - config: Partial; -} - -interface NodeErrorResponse extends ErrorResponse { - config: Partial; -} - -export type ConfigOrNodeErrorResponse = ConfigErrorResponse | NodeErrorResponse; - -interface Props { - errors?: ConfigOrNodeErrorResponse[]; -} - -type CombinedProps = Props; - -const NodeBalancerCreationError: React.FC = (props) => { - const { errors } = props; - - return !errors || errors.length === 0 ? null : ( - - {errors.map((errResponse, idx) => { - const message = isNodeError(errResponse) - ? `Unable to create node ${errResponse.config.label}.` - : `Unable to create config for port ${errResponse.config.port}.`; - - return ( - - {message} - {maybeListReason(errResponse.errors)} - - ); - })} - - ); -}; - -export default NodeBalancerCreationError; - -const maybeListReason = (errors?: APIError[]) => { - if (!errors || errors.length === 0) { - return null; - } - - return ( - - {errors.map((e) => ( - - - - ))} - - ); -}; - -const isNodeError = (v: ConfigOrNodeErrorResponse): v is NodeErrorResponse => - v.config.hasOwnProperty('label') && v.config.hasOwnProperty('address'); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx index 988cd276ee3..577babd324b 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx @@ -1,56 +1,45 @@ import * as React from 'react'; -import { createStyles, withStyles, WithStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; -import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import Grid from 'src/components/Grid'; -import { ExtendedNodeBalancer } from '../../types'; -import NodeBalancerCreationErrors, { - ConfigOrNodeErrorResponse, -} from './NodeBalancerCreationErrors'; import SummaryPanel from './SummaryPanel'; import TablesPanel from './TablesPanel'; +import { useParams } from 'react-router-dom'; +import { makeStyles } from '@mui/styles'; +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { useNodeBalancerQuery } from 'src/queries/nodebalancers'; +import { Theme } from '@mui/material/styles'; -type ClassNames = 'main' | 'sidebar'; - -const styles = (theme: Theme) => - createStyles({ - main: { - [theme.breakpoints.up('md')]: { - order: 1, - }, +const useStyles = makeStyles((theme: Theme) => ({ + main: { + [theme.breakpoints.up('md')]: { + order: 1, }, - sidebar: { - [theme.breakpoints.up('md')]: { - order: 2, - }, + }, + sidebar: { + [theme.breakpoints.up('md')]: { + order: 2, }, - }); - -interface Props { - nodeBalancer: ExtendedNodeBalancer; - errorResponses?: ConfigOrNodeErrorResponse[]; -} + }, +})); -type CombinedProps = Props & WithStyles; +const NodeBalancerSummary = () => { + const classes = useStyles(); + const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); + const id = Number(nodeBalancerId); + const { data: nodebalancer } = useNodeBalancerQuery(id); -const NodeBalancerSummary: React.FC = (props) => { - const { nodeBalancer, errorResponses, classes } = props; return (
- - + - + - +
); }; -const styled = withStyles(styles); - -export default styled(NodeBalancerSummary); +export default NodeBalancerSummary; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index 88bcc99c7ba..266d581d9ed 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -1,147 +1,150 @@ import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { compose } from 'recompose'; import Paper from 'src/components/core/Paper'; -import { createStyles, withStyles, WithStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import TagsPanel from 'src/components/TagsPanel'; -import summaryPanelStyles, { - StyleProps, -} from 'src/containers/SummaryPanels.styles'; +import summaryPanelStyles from 'src/containers/SummaryPanels.styles'; import IPAddress from 'src/features/linodes/LinodesLanding/IPAddress'; -import { ExtendedNodeBalancer } from 'src/features/NodeBalancers/types'; -import { useRegionsQuery } from 'src/queries/regions'; +import { Link, useParams } from 'react-router-dom'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; -import { NodeBalancerConsumer } from '../context'; - -type ClassNames = - | 'NBsummarySection' - | 'IPgrouping' - | 'nodeTransfer' - | 'hostName'; +import { Theme } from '@mui/material/styles'; +import { makeStyles } from '@mui/styles'; +import { useRegionsQuery } from 'src/queries/regions'; +import { + useAllNodeBalancerConfigsQuery, + useNodeBalancerQuery, + useNodebalancerUpdateMutation, +} from 'src/queries/nodebalancers'; -const styles = (theme: Theme) => - createStyles({ - ...summaryPanelStyles(theme), - root: { - paddingTop: theme.spacing(), - }, - NBsummarySection: { - [theme.breakpoints.up('md')]: { - marginTop: theme.spacing(6), - }, - }, - IPgrouping: { - margin: '-2px 0 0 2px', - display: 'flex', - flexDirection: 'column', - }, - nodeTransfer: { - marginTop: 12, - }, - hostName: { - wordBreak: 'break-word', +const useStyles = makeStyles((theme: Theme) => ({ + ...summaryPanelStyles(theme), + root: { + paddingTop: theme.spacing(), + }, + NBsummarySection: { + [theme.breakpoints.up('md')]: { + marginTop: theme.spacing(6), }, - }); + }, + IPgrouping: { + margin: '-2px 0 0 2px', + display: 'flex', + flexDirection: 'column', + }, + nodeTransfer: { + marginTop: 12, + }, + hostName: { + wordBreak: 'break-word', + }, +})); -interface Props { - nodeBalancer: ExtendedNodeBalancer; -} +const SummaryPanel = () => { + const classes = useStyles(); + const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); + const id = Number(nodeBalancerId); + const { data: nodebalancer } = useNodeBalancerQuery(id); + const { data: configs } = useAllNodeBalancerConfigsQuery(id); + const { data: regions } = useRegionsQuery(); -type CombinedProps = Props & StyleProps & WithStyles; + const region = regions?.find((r) => r.id === nodebalancer?.region); -const SummaryPanel: React.FC = (props) => { - const { nodeBalancer, classes } = props; - const { data: regions } = useRegionsQuery(); + const { mutateAsync: updateNodeBalancer } = useNodebalancerUpdateMutation(id); + + const configPorts = configs?.reduce((acc, config) => { + return [...acc, { configId: config.id, port: config.port }]; + }, []); + + const down = configs?.reduce((acc: number, config) => { + return acc + config.nodes_status.down; + }, 0); // add the downtime for each config together + + const up = configs?.reduce((acc: number, config) => { + return acc + config.nodes_status.up; + }, 0); // add the uptime for each config together - const region = regions?.find((r) => r.id === nodeBalancer.region); + if (!nodebalancer || !configs) { + return null; + } return ( - - {({ updateTags }) => { - return ( -
- - - NodeBalancer Details - -
- - Ports: - {nodeBalancer.configPorts.length === 0 && 'None'} - {nodeBalancer.configPorts.map(({ port, configId }, i) => ( - - - {port} - - {i < nodeBalancer.configPorts.length - 1 ? ', ' : ''} - - ))} - -
-
- - Backend Status: - {`${nodeBalancer.up} up, ${nodeBalancer.down} down`} - -
-
- - Transferred: - {convertMegabytesTo(nodeBalancer.transfer.total)} - -
-
- + + + NodeBalancer Details + +
+ + Ports: + {configPorts?.length === 0 && 'None'} + {configPorts?.map(({ port, configId }, i) => ( + + - Host Name: - {nodeBalancer.hostname} - -
-
- - Region:{' '} - {region?.label ?? nodeBalancer.region} - -
-
+ {port} + + {i < configPorts?.length - 1 ? ', ' : ''} + + ))} +
+
+
+ + Backend Status: + {`${up} up, ${down} down`} + +
+
+ + Transferred: + {convertMegabytesTo(nodebalancer.transfer.total)} + +
+
+ + Host Name: + {nodebalancer.hostname} + +
+
+ + Region: {region?.label} + +
+
- - - IP Addresses - -
-
- - {nodeBalancer.ipv6 && } -
-
-
- - - - Tags - - - + + + IP Addresses + +
+
+ {nodebalancer?.ipv4 && ( + + )} + {nodebalancer?.ipv6 && }
- ); - }} - +
+
+ + + + Tags + + updateNodeBalancer({ tags })} + /> + +
); }; -const localStyles = withStyles(styles); - -const enhanced = compose(localStyles); - -export default enhanced(SummaryPanel); +export default SummaryPanel; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx index 535eb0dceee..fb54d80ac0e 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import CircleProgress from 'src/components/CircleProgress'; import Paper from 'src/components/core/Paper'; import { makeStyles } from '@mui/styles'; -import { Theme, useTheme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import ErrorState from 'src/components/ErrorState'; import Grid from 'src/components/Grid'; @@ -11,14 +10,16 @@ import MetricsDisplay from 'src/components/LineGraph/MetricsDisplay'; import getUserTimezone from 'src/utilities/getUserTimezone'; import PendingIcon from 'src/assets/icons/pending.svg'; import { formatBitsPerSecond } from 'src/features/Longview/shared/utilities'; -import { ExtendedNodeBalancer } from 'src/features/NodeBalancers/types'; import { useProfile } from 'src/queries/profile'; import { formatNumber, getMetrics } from 'src/utilities/statMetrics'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { NODEBALANCER_STATS_NOT_READY_API_MESSAGE, + useNodeBalancerQuery, useNodeBalancerStats, } from 'src/queries/nodebalancers'; +import { useParams } from 'react-router-dom'; +import { Theme, useTheme } from '@mui/material/styles'; const useStyles = makeStyles((theme: Theme) => ({ header: { @@ -75,19 +76,18 @@ const Loading = () => ( const STATS_NOT_READY_TITLE = 'Stats for this NodeBalancer are not available yet'; -interface Props { - nodeBalancer: ExtendedNodeBalancer; -} - -const TablesPanel: React.FC = ({ nodeBalancer }) => { +const TablesPanel = () => { const classes = useStyles(); const theme = useTheme(); const { data: profile } = useProfile(); const timezone = getUserTimezone(profile); + const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); + const id = Number(nodeBalancerId); + const { data: nodebalancer } = useNodeBalancerQuery(id); const { data: stats, isLoading, error } = useNodeBalancerStats( - nodeBalancer.id, - nodeBalancer.created + nodebalancer?.id ?? -1, + nodebalancer?.created ); const statsErrorString = error diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/index.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/index.tsx deleted file mode 100644 index 716411603b4..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import NodeBalancerSummary from './NodeBalancerSummary'; -export default NodeBalancerSummary; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/context.ts b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/context.ts deleted file mode 100644 index a011ae119e0..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/context.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from 'react'; - -const { Provider, Consumer } = React.createContext(null); - -export const NodeBalancerProvider = Provider; -export const NodeBalancerConsumer = Consumer; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/index.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/index.tsx deleted file mode 100644 index 7cc04a401d9..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import NodeBalancerDetail from './NodeBalancerDetail'; -export default NodeBalancerDetail; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx index 1964ee348e5..5365b41dc56 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx @@ -1,114 +1,87 @@ -import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; -import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; -import { compose } from 'recompose'; -import EnhancedSelect, { Item } from 'src/components/EnhancedSelect/Select'; -import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; -import { Props as TextFieldProps } from 'src/components/TextField'; -import withNodeBalancers from 'src/containers/withNodeBalancers.container'; -import { - WithNodeBalancerActions, - withNodeBalancerActions, -} from 'src/store/nodeBalancer/nodeBalancer.containers'; -import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; - -interface WithNodeBalancersProps { - nodeBalancersData: NodeBalancer[]; - nodeBalancersLoading: boolean; - nodeBalancersError?: APIError[]; - nodeBalancersLastUpdated: number; -} +import TextField, { Props as TextFieldProps } from 'src/components/TextField'; +import Autocomplete from '@mui/material/Autocomplete'; +import { useInfiniteNodebalancersQuery } from 'src/queries/nodebalancers'; +import { NodeBalancer } from '@linode/api-v4'; interface Props { - generalError?: string; - nodeBalancerError?: string; - selectedNodeBalancer: number | null; + error?: string; + value?: number; disabled?: boolean; region?: string; - handleChange: (nodeBalancer: NodeBalancer) => void; + onChange: (id: number, nodebalancer: NodeBalancer | undefined) => void; textFieldProps?: TextFieldProps; } -type CombinedProps = Props & WithNodeBalancersProps & WithNodeBalancerActions; +export const NodeBalancerSelect = (props: Props) => { + const { disabled, error, onChange, value, region } = props; + const [inputValue, setInputValue] = React.useState(''); -const nodeBalancersToItems = (nodeBalancers: NodeBalancer[]): Item[] => - nodeBalancers.map((thisNodeBalancer) => ({ - value: thisNodeBalancer.id, - label: thisNodeBalancer.label, - data: thisNodeBalancer, - })); - -const nodeBalancerFromItems = ( - nodeBalancers: Item[], - nodeBalancerId: number | null -) => { - if (!nodeBalancerId) { - return; - } - return nodeBalancers.find( - (thisNodeBalancer) => thisNodeBalancer.value === nodeBalancerId - ); -}; + const searchFilter = inputValue + ? { + '+or': [ + { label: { '+contains': inputValue } }, + { tags: { '+contains': inputValue } }, + ], + } + : {}; -const NodeBalancerSelect: React.FC = (props) => { const { - disabled, - generalError, - handleChange, - nodeBalancerError, - nodeBalancersError, - nodeBalancersLoading, - nodeBalancersLastUpdated, - nodeBalancersData, - nodeBalancerActions, - region, - selectedNodeBalancer, - } = props; - - React.useEffect(() => { - // If NodeBalacers have not yet been requested when this component was mounted, - // make the API call to get them. - if (nodeBalancersLastUpdated === 0) { - nodeBalancerActions.getAllNodeBalancers(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + data, + isLoading, + fetchNextPage, + hasNextPage, + } = useInfiniteNodebalancersQuery({ + ...searchFilter, + ...(region ? { region } : {}), + '+order_by': 'label', + '+order': 'asc', + }); + const nodebalancers = data?.pages.flatMap((page) => page.data); - const nodeBalancer = region - ? nodeBalancersData.filter( - (thisNodeBalancer) => thisNodeBalancer.region === region - ) - : nodeBalancersData; - const options = nodeBalancersToItems(nodeBalancer); + const options = nodebalancers?.map(({ id, label }) => ({ id, label })); - const noOptionsMessage = - !nodeBalancerError && !nodeBalancersLoading && options.length === 0 - ? 'You have no NodeBalancers to choose from' - : 'No Options'; + const selectedNodebalancer = + options?.find((option) => option.id === value) ?? null; return ( - ) => { - return handleChange(selected.data); + options={options ?? []} + value={selectedNodebalancer} + onChange={(event, value) => + onChange( + value?.id ?? -1, + nodebalancers?.find((n) => n.id === value?.id) + ) + } + inputValue={inputValue} + onInputChange={(event, value) => { + setInputValue(value); }} - errorText={getErrorStringOrDefault( - generalError || nodeBalancerError || nodeBalancersError || '' + isOptionEqualToValue={(option) => option.id === selectedNodebalancer?.id} + ListboxProps={{ + onScroll: (event: React.SyntheticEvent) => { + const listboxNode = event.currentTarget; + if ( + listboxNode.scrollTop + listboxNode.clientHeight >= + listboxNode.scrollHeight && + hasNextPage + ) { + fetchNextPage(); + } + }, + }} + loading={isLoading} + renderInput={(params) => ( + )} - isClearable={false} - textFieldProps={props.textFieldProps} - noOptionsMessage={() => noOptionsMessage} /> ); }; - -export default compose( - RenderGuard, - withNodeBalancerActions, - withNodeBalancers() -)(NodeBalancerSelect); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx index 6cae06a51eb..308fef05c2e 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx @@ -1,37 +1,32 @@ import * as React from 'react'; -import { - Route, - RouteComponentProps, - Switch, - withRouter, -} from 'react-router-dom'; -import SuspenseLoader from 'src/components/SuspenseLoader'; +import { Route, Switch } from 'react-router-dom'; +import CircleProgress from 'src/components/CircleProgress'; -const NodeBalancerDetail = React.lazy(() => import('./NodeBalancerDetail')); -const NodeBalancersLanding = React.lazy(() => import('./NodeBalancersLanding')); +const NodeBalancerDetail = React.lazy( + () => import('./NodeBalancerDetail/NodeBalancerDetail') +); +const NodeBalancersLanding = React.lazy( + () => import('./NodeBalancersLanding/NodeBalancersLanding') +); const NodeBalancerCreate = React.lazy(() => import('./NodeBalancerCreate')); -type Props = RouteComponentProps<{}>; +const NodeBalancers = () => { + return ( + }> + + + + + + + ); +}; -class NodeBalancers extends React.Component { - render() { - const { - match: { path }, - } = this.props; - - return ( - }> - - - - - - - ); - } -} - -export default withRouter(NodeBalancers); +export default NodeBalancers; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx index 60d2e510e72..78fb489a8fe 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import ActionMenu, { Action } from 'src/components/ActionMenu'; import Hidden from 'src/components/core/Hidden'; -import { useTheme } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import InlineMenuAction from 'src/components/InlineMenuAction'; +import { useHistory } from 'react-router-dom'; +import { useTheme } from '@mui/styles'; +import { Theme } from '@mui/material/styles'; interface Props { nodeBalancerId: number; @@ -13,13 +13,13 @@ interface Props { label: string; } -type CombinedProps = Props & RouteComponentProps<{}>; - -export const NodeBalancerActionMenu: React.FC = (props) => { +export const NodeBalancerActionMenu = (props: Props) => { const theme = useTheme(); const matchesMdDown = useMediaQuery(theme.breakpoints.down('lg')); - const { nodeBalancerId, history, toggleDialog, label } = props; + const history = useHistory(); + + const { nodeBalancerId, toggleDialog, label } = props; const actions: Action[] = [ { @@ -63,5 +63,3 @@ export const NodeBalancerActionMenu: React.FC = (props) => { ); }; - -export default withRouter(NodeBalancerActionMenu); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx index 4dafe6c872d..1e71497958c 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx @@ -1,17 +1,18 @@ -import { NodeBalancerWithConfigs } from '@linode/api-v4/lib/nodebalancers'; import * as React from 'react'; +import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; import { Link } from 'react-router-dom'; import Hidden from 'src/components/core/Hidden'; -import { makeStyles } from '@mui/styles'; -import { Theme } from '@mui/material/styles'; +import { makeStyles } from 'tss-react/mui'; import TableCell from 'src/components/TableCell'; import TableRow from 'src/components/TableRow'; import IPAddress from 'src/features/linodes/LinodesLanding/IPAddress'; import RegionIndicator from 'src/features/linodes/LinodesLanding/RegionIndicator'; +import { useAllNodeBalancerConfigsQuery } from 'src/queries/nodebalancers'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; -import NodeBalancerActionMenu from './NodeBalancerActionMenu'; +import { NodeBalancerActionMenu } from './NodeBalancerActionMenu'; +import { Theme } from '@mui/material/styles'; -const useStyles = makeStyles((theme: Theme) => ({ +const useStyles = makeStyles()((theme: Theme) => ({ // @todo: temporary measure that will cause scroll for the 'Name' and 'Backend Status' // column until we implement a hideOnTablet prop for EntityTables to prevent the // ActionCell from being misaligned @@ -54,21 +55,21 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -interface Props { - toggleDialog: (id: number, label: string) => void; +interface Props extends NodeBalancer { + onDelete: (id: number) => void; } -type CombinedProps = NodeBalancerWithConfigs & Props; +export const NodeBalancerTableRow = (props: Props) => { + const { classes } = useStyles(); + const { id, label, transfer, ipv4, region, onDelete } = props; -const NodeBalancerTableRow: React.FC = (props) => { - const classes = useStyles(); - const { id, label, configs, transfer, ipv4, region, toggleDialog } = props; + const { data: configs } = useAllNodeBalancerConfigsQuery(id); - const nodesUp = configs.reduce( + const nodesUp = configs?.reduce( (result, config) => config.nodes_status.up + result, 0 ); - const nodesDown = configs.reduce( + const nodesDown = configs?.reduce( (result, config) => config.nodes_status.down + result, 0 ); @@ -77,7 +78,7 @@ const NodeBalancerTableRow: React.FC = (props) => { @@ -91,7 +92,6 @@ const NodeBalancerTableRow: React.FC = (props) => { - {nodesUp} up - {nodesDown} down @@ -101,10 +101,9 @@ const NodeBalancerTableRow: React.FC = (props) => { {convertMegabytesTo(transfer.total)} - - {configs.length === 0 && 'None'} - {configs.map(({ port, id: configId }, i) => ( + {configs?.length === 0 && 'None'} + {configs?.map(({ port, id: configId }, i) => ( = (props) => { ))} -
@@ -128,16 +126,13 @@ const NodeBalancerTableRow: React.FC = (props) => { - ); }; - -export default NodeBalancerTableRow; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx deleted file mode 100644 index 37373427e6b..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { mount } from 'enzyme'; -import * as React from 'react'; -import { Provider } from 'react-redux'; -import { StaticRouter } from 'react-router-dom'; -import LinodeThemeWrapper from 'src/LinodeThemeWrapper'; -import store from 'src/store'; -import { reactRouterProps } from 'src/__data__/reactRouterProps'; -import { NodeBalancersLanding } from './NodeBalancersLanding'; - -describe.skip('NodeBalancers', () => { - const component = mount( - - - - - - - - ); - - it('should render 7 columns', () => { - const numOfColumns = component - .find('WithStyles(TableHead)') - .find('WithStyles(TableCell)').length; - expect(numOfColumns).toBe(7); - }); - - it.skip('should render a Kabob menu', () => { - const kabobMenu = component - .find('withRouter(NodeBalancerActionMenu)') - .first(); - expect(kabobMenu).toHaveLength(1); - }); -}); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx index 0b58d221dd9..ea8d8a49ef9 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx @@ -1,356 +1,156 @@ -import { - NodeBalancer, - NodeBalancerConfig, -} from '@linode/api-v4/lib/nodebalancers'; -import { APIError } from '@linode/api-v4/lib/types'; -import { path } from 'ramda'; import * as React from 'react'; -import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { compose } from 'recompose'; +import { useHistory } from 'react-router-dom'; import CircleProgress from 'src/components/CircleProgress'; -import Typography from 'src/components/core/Typography'; +import Hidden from 'src/components/core/Hidden'; +import TableBody from 'src/components/core/TableBody'; +import TableHead from 'src/components/core/TableHead'; +import TableRow from 'src/components/core/TableRow'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import EntityTable, { - EntityTableRow, - HeaderCell, -} from 'src/components/EntityTable'; import ErrorState from 'src/components/ErrorState'; import LandingHeader from 'src/components/LandingHeader'; -import Notice from 'src/components/Notice'; -import PreferenceToggle, { ToggleProps } from 'src/components/PreferenceToggle'; -import SectionErrorBoundary from 'src/components/SectionErrorBoundary'; +import PaginationFooter from 'src/components/PaginationFooter'; +import Table from 'src/components/Table/Table'; +import TableCell from 'src/components/TableCell/TableCell'; +import { TableSortCell } from 'src/components/TableSortCell/TableSortCell'; import TransferDisplay from 'src/components/TransferDisplay'; -import TypeToConfirmDialog from 'src/components/TypeToConfirmDialog'; -import { ApplicationState } from 'src/store'; -import { - withNodeBalancerActions, - WithNodeBalancerActions, -} from 'src/store/nodeBalancer/nodeBalancer.containers'; -import { nodeBalancersWithConfigs } from 'src/store/nodeBalancer/nodeBalancer.selectors'; +import { useOrder } from 'src/hooks/useOrder'; +import usePagination from 'src/hooks/usePagination'; +import { useNodeBalancersQuery } from 'src/queries/nodebalancers'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import { sendGroupByTagEnabledEvent } from 'src/utilities/ga'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; import NodeBalancersLandingEmptyState from './NodeBalancersLandingEmptyState'; -import NodeBalancerTableRow from './NodeBalancerTableRow'; +import { NodeBalancerTableRow } from './NodeBalancerTableRow'; -interface DeleteConfirmDialogState { - open: boolean; - submitting: boolean; - errors?: APIError[]; -} +const preferenceKey = 'nodebalancers'; -interface State { - deleteConfirmDialog: DeleteConfirmDialogState; - selectedNodeBalancerId?: number; - selectedNodeBalancerLabel: string; -} - -type CombinedProps = WithNodeBalancerActions & - WithNodeBalancers & - RouteComponentProps<{}>; - -export const headers: HeaderCell[] = [ - { - label: 'Name', - dataColumn: 'label', - sortable: true, - widthPercent: 20, - }, - { - label: 'Backend Status', - dataColumn: 'status', - sortable: false, - widthPercent: 15, - hideOnMobile: true, - }, - { - label: 'Transferred', - dataColumn: 'transfer:total', - sortable: true, - widthPercent: 5, - hideOnMobile: true, - hideOnTablet: true, - }, - { - label: 'Ports', - dataColumn: 'updated', - sortable: true, - widthPercent: 5, - hideOnMobile: true, - hideOnTablet: true, - }, - { - label: 'IP Address', - dataColumn: 'ip', - sortable: false, - widthPercent: 5, - }, - { - label: 'Region', - dataColumn: 'region', - sortable: true, - widthPercent: 5, - hideOnMobile: true, - }, -]; - -export class NodeBalancersLanding extends React.Component< - CombinedProps, - State -> { - static defaultDeleteConfirmDialogState = { - submitting: false, - open: false, - errors: undefined, - }; - - state: State = { - deleteConfirmDialog: NodeBalancersLanding.defaultDeleteConfirmDialogState, - selectedNodeBalancerLabel: '', - }; - - pollInterval: number; - - componentDidMount() { - const { getAllNodeBalancersWithConfigs } = this.props.nodeBalancerActions; - /** - * Normally we check for lastUpdated === 0 before requesting data, - * but since this page is already polling every 30 seconds (@todo should it?), - * it seems counterintuitive to make the first request conditional. - */ - getAllNodeBalancersWithConfigs(); - /** - * To keep NB node status up to date, poll NodeBalancers and configs every 30 seconds while the - * user is on this page. - */ - this.pollInterval = window.setInterval(() => { - if (document.visibilityState === 'visible') { - getAllNodeBalancersWithConfigs(); - } - }, 30 * 1000); - } - - componentWillUnmount() { - clearInterval(this.pollInterval); - } +export const NodeBalancersLanding = () => { + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState( + false + ); + const [ + selectedNodeBalancerId, + setSelectedNodeBalancerId, + ] = React.useState(-1); + + const history = useHistory(); + const pagination = usePagination(1, preferenceKey); + + const { order, orderBy, handleOrderChange } = useOrder( + { + orderBy: 'label', + order: 'asc', + }, + `${preferenceKey}-order` + ); - toggleDialog = (nodeBalancerId: number, label: string) => { - this.setState({ - selectedNodeBalancerId: nodeBalancerId, - selectedNodeBalancerLabel: label, - deleteConfirmDialog: { - ...this.state.deleteConfirmDialog, - open: !this.state.deleteConfirmDialog.open, - }, - }); + const filter = { + ['+order_by']: orderBy, + ['+order']: order, }; - onSubmitDelete = () => { - const { - nodeBalancerActions: { deleteNodeBalancer }, - } = this.props; - const { selectedNodeBalancerId } = this.state; - - if (!selectedNodeBalancerId) { - return; - } + const { data, isLoading, error } = useNodeBalancersQuery( + { + page: pagination.page, + page_size: pagination.pageSize, + }, + filter + ); - this.setState({ - deleteConfirmDialog: { - ...this.state.deleteConfirmDialog, - errors: undefined, - submitting: true, - }, - }); + const selectedNodeBalancer = data?.data.find( + (nodebalancer) => nodebalancer.id === selectedNodeBalancerId + ); - deleteNodeBalancer({ nodeBalancerId: selectedNodeBalancerId }) - .then((_) => { - this.setState({ - deleteConfirmDialog: { - open: false, - submitting: false, - }, - }); - }) - .catch((err) => { - return this.setState( - { - deleteConfirmDialog: { - ...this.state.deleteConfirmDialog, - submitting: false, - errors: getAPIErrorOrDefault( - err, - 'There was an error deleting this NodeBalancer.' - ), - }, - }, - () => { - scrollErrorIntoView(); - } - ); - }); + const onDelete = (nodeBalancerId: number) => { + setSelectedNodeBalancerId(nodeBalancerId); + setIsDeleteDialogOpen(true); }; - render() { - const { - nodeBalancersCount, - nodeBalancersLoading, - nodeBalancersData, - nodeBalancersLastUpdated, - nodeBalancersError, - } = this.props; - - const { - deleteConfirmDialog: { open: deleteConfirmAlertOpen }, - } = this.state; - - if (nodeBalancersError) { - return ; - } - - if (nodeBalancersLoading) { - return ; - } - - if (nodeBalancersCount === 0) { - return ; - } - - const nodeBalancerRow: EntityTableRow = { - Component: NodeBalancerTableRow, - data: Object.values(nodeBalancersData), - handlers: { toggleDialog: this.toggleDialog }, - loading: nodeBalancersLoading, - error: nodeBalancersError, - lastUpdated: nodeBalancersLastUpdated, - }; - + if (error) { return ( - <> - - - preferenceKey="nodebalancers_group_by_tag" - preferenceOptions={[false, true]} - localStorageKey="GROUP_NODEBALANCERS" - toggleCallbackFnDebounced={toggleNodeBalancersGroupBy} - > - {({ - preference: nodeBalancersAreGrouped, - togglePreference: toggleNodeBalancerGroupByTag, - }: ToggleProps) => { - return ( - <> - - this.props.history.push('/nodebalancers/create') - } - docsLink="https://www.linode.com/docs/platform/nodebalancer/getting-started-with-nodebalancers/" - /> - - - ); - }} - - - - - - Deleting this NodeBalancer is permanent and can’t be undone. - - - - Traffic will no longer be routed through this NodeBalancer. Please - check your DNS settings and either provide the IP address of another - active NodeBalancer, or route traffic directly to your Linode. - - - + ); } - closeConfirmationDialog = () => - this.setState({ - deleteConfirmDialog: NodeBalancersLanding.defaultDeleteConfirmDialogState, - }); -} - -const eventCategory = `nodebalancers landing`; - -const toggleNodeBalancersGroupBy = (checked: boolean) => - sendGroupByTagEnabledEvent(eventCategory, checked); - -interface NodeBalancerWithConfigs extends NodeBalancer { - configs: NodeBalancerConfig[]; -} - -interface WithNodeBalancers { - nodeBalancersCount: number; - nodeBalancersData: NodeBalancerWithConfigs[]; - nodeBalancersError?: APIError[]; - nodeBalancersLoading: boolean; - nodeBalancersLastUpdated: number; -} - -export const enhanced = compose( - connect((state: ApplicationState) => { - const { __resources } = state; - const { nodeBalancers } = __resources; - const { - error, - results, - loading: nodeBalancersLoading, - lastUpdated, - } = nodeBalancers; - - return { - nodeBalancersCount: results, - nodeBalancersData: nodeBalancersWithConfigs(__resources), - nodeBalancersError: path(['read'], error), - // In this component we only want to show loading state on initial load - nodeBalancersLoading: nodeBalancersLoading && lastUpdated === 0, - nodeBalancersLastUpdated: lastUpdated, - }; - }), - withRouter, - withNodeBalancerActions, - SectionErrorBoundary -); - -export default enhanced(NodeBalancersLanding); + if (isLoading) { + return ; + } -const LoadingState = () => { - return ; -}; + if (data?.results === 0) { + return ; + } -const RenderError = ({ errors }: { errors: APIError[] }) => { return ( - + <> + + history.push('/nodebalancers/create')} + docsLink="https://www.linode.com/docs/platform/nodebalancer/getting-started-with-nodebalancers/" + /> + + + + + Label + + + Backend Status + + + Transfered + Ports + + IP Address + + + Region + + + + + + + {data?.data.map((nodebalancer) => ( + + ))} + +
+ + + setIsDeleteDialogOpen(false)} + /> + ); }; + +export default NodeBalancersLanding; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/index.ts b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/index.ts deleted file mode 100644 index 33a1ea5c0db..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './NodeBalancersLanding'; diff --git a/packages/manager/src/features/NodeBalancers/index.tsx b/packages/manager/src/features/NodeBalancers/index.tsx deleted file mode 100644 index 4c3a7541b03..00000000000 --- a/packages/manager/src/features/NodeBalancers/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import NodeBalancers from './NodeBalancers'; -export default NodeBalancers; diff --git a/packages/manager/src/features/NodeBalancers/types.ts b/packages/manager/src/features/NodeBalancers/types.ts index b1276d60f69..102303e5cf0 100644 --- a/packages/manager/src/features/NodeBalancers/types.ts +++ b/packages/manager/src/features/NodeBalancers/types.ts @@ -1,6 +1,4 @@ import { - NodeBalancer, - NodeBalancerConfigPort, NodeBalancerConfigNodeMode, NodeBalancerProxyProtocol, } from '@linode/api-v4/lib/nodebalancers/types'; @@ -54,8 +52,3 @@ export interface NodeBalancerConfigNodeFields { errors?: APIError[]; status?: 'UP' | 'DOWN' | 'unknown'; } -export interface ExtendedNodeBalancer extends NodeBalancer { - up: number; - down: number; - configPorts: NodeBalancerConfigPort[]; -} diff --git a/packages/manager/src/features/Search/SearchLanding.tsx b/packages/manager/src/features/Search/SearchLanding.tsx index 4a327424178..befdbd8913e 100644 --- a/packages/manager/src/features/Search/SearchLanding.tsx +++ b/packages/manager/src/features/Search/SearchLanding.tsx @@ -39,6 +39,7 @@ import { emptyResults } from './utils'; import withStoreSearch, { SearchProps } from './withStoreSearch'; import { extendTypesQueryResult } from 'src/utilities/extendType'; import { isNotNullOrUndefined } from 'src/utilities/nullOrUndefined'; +import { useAllNodeBalancersQuery } from 'src/queries/nodebalancers'; const useStyles = makeStyles((theme: Theme) => ({ root: { @@ -127,6 +128,12 @@ export const SearchLanding: React.FC = (props) => { isLoading: areKubernetesClustersLoading, } = useAllKubernetesClustersQuery(!_isLargeAccount); + const { + data: nodebalancers, + error: nodebalancersError, + isLoading: areNodeBalancersLoading, + } = useAllNodeBalancersQuery(!_isLargeAccount); + const { data: volumes, isLoading: areVolumesLoading, @@ -173,7 +180,7 @@ export const SearchLanding: React.FC = (props) => { } const { _loading: reduxLoading } = useReduxLoad( - ['linodes', 'nodeBalancers'], + ['linodes'], REFRESH_INTERVAL, !_isLargeAccount ); @@ -211,7 +218,8 @@ export const SearchLanding: React.FC = (props) => { kubernetesClusters ?? [], _privateImages ?? [], regions ?? [], - searchableLinodes ?? [] + searchableLinodes ?? [], + nodebalancers ?? [] ); } }, [ @@ -226,6 +234,7 @@ export const SearchLanding: React.FC = (props) => { kubernetesClusters, _privateImages, regions, + nodebalancers, ]); const getErrorMessage = (errors: ErrorObject): string => { @@ -242,7 +251,7 @@ export const SearchLanding: React.FC = (props) => { if (imagesError) { errorString.push('Images'); } - if (errors.nodebalancers) { + if (nodebalancersError) { errorString.push('NodeBalancers'); } if (kubernetesClustersError) { @@ -273,7 +282,8 @@ export const SearchLanding: React.FC = (props) => { areDomainsLoading || areVolumesLoading || areKubernetesClustersLoading || - areImagesLoading; + areImagesLoading || + areNodeBalancersLoading; return ( diff --git a/packages/manager/src/features/Search/withStoreSearch.tsx b/packages/manager/src/features/Search/withStoreSearch.tsx index 7ccea7557fb..83e46b50f80 100644 --- a/packages/manager/src/features/Search/withStoreSearch.tsx +++ b/packages/manager/src/features/Search/withStoreSearch.tsx @@ -1,19 +1,21 @@ -import { Image, KubernetesCluster, Region, Volume } from '@linode/api-v4'; +import { + Image, + KubernetesCluster, + NodeBalancer, + Region, + Volume, +} from '@linode/api-v4'; import { Domain } from '@linode/api-v4/lib/domains'; import { ObjectStorageBucket } from '@linode/api-v4/lib/object-storage'; import * as React from 'react'; -import { connect } from 'react-redux'; import { compose, withStateHandlers } from 'recompose'; -import { ApplicationState } from 'src/store'; -import entitiesErrors, { - ErrorObject, -} from 'src/store/selectors/entitiesErrors'; -import entitiesLoading from 'src/store/selectors/entitiesLoading'; -import getSearchEntities, { +import { ErrorObject } from 'src/store/selectors/entitiesErrors'; +import { bucketToSearchableItem, domainToSearchableItem, imageToSearchableItem, kubernetesClusterToSearchableItem, + nodeBalToSearchableItem, volumeToSearchableItem, } from 'src/store/selectors/getSearchEntities'; import { refinedSearch } from './refinedSearch'; @@ -33,7 +35,8 @@ interface HandlerProps { clusters: KubernetesCluster[], images: Image[], regions: Region[], - searchableLinodes: SearchableItem[] + searchableLinodes: SearchableItem[], + nodebalancers: NodeBalancer[] ) => SearchResults; } export interface SearchProps extends HandlerProps { @@ -67,20 +70,11 @@ export default () => (Component: React.ComponentType) => { }); }; - const connected = connect((state: ApplicationState) => { - return { - entities: getSearchEntities(state.__resources), - entitiesLoading: entitiesLoading(state.__resources), - errors: entitiesErrors(state.__resources), - }; - }); - return compose( - connected, withStateHandlers( { searchResultsByEntity: emptyResults }, { - search: (_, props: SearchProps) => ( + search: (_) => ( query: string, objectStorageBuckets: ObjectStorageBucket[], domains: Domain[], @@ -88,7 +82,8 @@ export default () => (Component: React.ComponentType) => { clusters: KubernetesCluster[], images: Image[], regions: Region[], - searchableLinodes: SearchableItem[] + searchableLinodes: SearchableItem[], + nodebalancers: NodeBalancer[] ) => { const searchableBuckets = objectStorageBuckets.map((bucket) => bucketToSearchableItem(bucket) @@ -106,15 +101,19 @@ export default () => (Component: React.ComponentType) => { const searchableClusters = clusters.map((cluster) => kubernetesClusterToSearchableItem(cluster, regions) ); + + const searchableNodebalancers = nodebalancers.map((nodebalancer) => + nodeBalToSearchableItem(nodebalancer) + ); const results = search( [ ...searchableLinodes, ...searchableImages, - ...props.entities, ...searchableBuckets, ...searchableDomains, ...searchableVolumes, ...searchableClusters, + ...searchableNodebalancers, ], query ); diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx index 4ba56cdb414..cf56fa2c369 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx @@ -45,6 +45,7 @@ import SupportTicketSMTPFields, { fieldNameToLabelMap, smtpHelperText, } from './SupportTicketSMTPFields'; +import { useAllNodeBalancersQuery } from 'src/queries/nodebalancers'; const useStyles = makeStyles((theme: Theme) => ({ expPanelSummary: { @@ -254,6 +255,10 @@ export const SupportTicketDrawer: React.FC = (props) => { const { data: domains, isLoading: domainsLoading } = useAllDomainsQuery( entityType === 'domain_id' ); + const { + data: nodebalancers, + isLoading: nodebalancersLoading, + } = useAllNodeBalancersQuery(entityType === 'nodebalancer_id'); const { data: clusters, @@ -312,10 +317,7 @@ export const SupportTicketDrawer: React.FC = (props) => { handleSetOrRequestEntities(entities.linodes, _entityType); return; } - case 'nodebalancer_id': { - handleSetOrRequestEntities(entities.nodeBalancers, _entityType); - return; - } + case 'nodebalancer_id': case 'lkecluster_id': case 'volume_id': case 'firewall_id': @@ -569,6 +571,7 @@ export const SupportTicketDrawer: React.FC = (props) => { domain_id: domains, volume_id: volumes, lkecluster_id: clusters, + nodebalancer_id: nodebalancers, }; if (!reactQueryEntityDataMap[entityType]) { @@ -612,6 +615,9 @@ export const SupportTicketDrawer: React.FC = (props) => { if (entityType === 'lkecluster_id') { return clustersLoading; } + if (entityType === 'nodebalancer_id') { + return nodebalancersLoading; + } return entitiesLoading; }; diff --git a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx index 34b272db1da..fe9f615bb8b 100644 --- a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx +++ b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx @@ -36,6 +36,7 @@ import { useSpecificTypes } from 'src/queries/types'; import { extendTypesQueryResult } from 'src/utilities/extendType'; import { isNotNullOrUndefined } from 'src/utilities/nullOrUndefined'; import { useRegionsQuery } from 'src/queries/regions'; +import { useAllNodeBalancersQuery } from 'src/queries/nodebalancers'; type CombinedProps = SearchProps & StyleProps & RouteComponentProps; @@ -73,7 +74,7 @@ export const selectStyles = { menu: (base: any) => ({ ...base, maxWidth: '100% !important' }), }; -const searchDeps: ReduxEntity[] = ['linodes', 'nodeBalancers']; +const searchDeps: ReduxEntity[] = ['linodes']; export const SearchBar: React.FC = (props) => { const { classes, combinedResults, entitiesLoading, search } = props; @@ -106,6 +107,8 @@ export const SearchBar: React.FC = (props) => { const { data: volumes } = useAllVolumesQuery({}, {}, shouldMakeRequests); + const { data: nodebalancers } = useAllNodeBalancersQuery(shouldMakeRequests); + const { data: _privateImages, isLoading: imagesLoading } = useAllImagesQuery( {}, { is_public: false }, // We want to display private images (i.e., not Debian, Ubuntu, etc. distros) @@ -177,7 +180,8 @@ export const SearchBar: React.FC = (props) => { clusters ?? [], _privateImages ?? [], regions ?? [], - searchableLinodes ?? [] + searchableLinodes ?? [], + nodebalancers ?? [] ); } }, [ @@ -192,6 +196,7 @@ export const SearchBar: React.FC = (props) => { volumes, _privateImages, regions, + nodebalancers, ]); const handleSearchChange = (_searchText: string): void => { diff --git a/packages/manager/src/hooks/useEntities.ts b/packages/manager/src/hooks/useEntities.ts index 0d69e5b6c0f..1fcc5088887 100644 --- a/packages/manager/src/hooks/useEntities.ts +++ b/packages/manager/src/hooks/useEntities.ts @@ -1,7 +1,6 @@ import { APIError } from '@linode/api-v4/lib/types'; import useLinodeActions from './useLinodeActions'; import useLinodes from './useLinodes'; -import useNodeBalancers from './useNodeBalancers'; export interface Entity { data: T[]; @@ -23,10 +22,6 @@ export interface Entity { export const useEntities = () => { const { linodes: _linodes } = useLinodes(); const { requestLinodes } = useLinodeActions(); - const { - nodeBalancers: _nodeBalancers, - requestNodeBalancers, - } = useNodeBalancers(); /** Our Redux store is currently inconsistent about * the data shape for different entity types. @@ -38,7 +33,6 @@ export const useEntities = () => { */ const linodes = Object.values(_linodes.itemsById); - const nodeBalancers = Object.values(_nodeBalancers.itemsById); return { linodes: { @@ -47,12 +41,6 @@ export const useEntities = () => { lastUpdated: _linodes.lastUpdated, error: _linodes.error?.read, }, - nodeBalancers: { - data: nodeBalancers, - request: () => requestNodeBalancers().then((response) => response.data), - lastUpdated: _nodeBalancers.lastUpdated, - error: _nodeBalancers.error?.read, - }, }; }; diff --git a/packages/manager/src/hooks/useNodeBalancers.ts b/packages/manager/src/hooks/useNodeBalancers.ts deleted file mode 100644 index 743055c2c6b..00000000000 --- a/packages/manager/src/hooks/useNodeBalancers.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers/types'; -import { useDispatch, useSelector } from 'react-redux'; -import { ApplicationState } from 'src/store'; -import { State } from 'src/store/nodeBalancer/nodeBalancer.reducer'; -import { getAllNodeBalancers as _request } from 'src/store/nodeBalancer/nodeBalancer.requests'; -import { Dispatch } from './types'; - -export interface NodeBalancersProps { - nodeBalancers: State; - requestNodeBalancers: () => Promise; -} - -export const useNodeBalancers = () => { - const dispatch: Dispatch = useDispatch(); - const nodeBalancers = useSelector( - (state: ApplicationState) => state.__resources.nodeBalancers - ); - const requestNodeBalancers = () => dispatch(_request()); - - return { nodeBalancers, requestNodeBalancers }; -}; - -export default useNodeBalancers; diff --git a/packages/manager/src/hooks/useReduxLoad.ts b/packages/manager/src/hooks/useReduxLoad.ts index fddec3fc850..600db102210 100644 --- a/packages/manager/src/hooks/useReduxLoad.ts +++ b/packages/manager/src/hooks/useReduxLoad.ts @@ -7,19 +7,17 @@ import { ApplicationState } from 'src/store'; import { getEvents } from 'src/store/events/event.request'; import { requestLinodes } from 'src/store/linodes/linode.requests'; import { getAllLongviewClients } from 'src/store/longview/longview.requests'; -import { getAllNodeBalancers } from 'src/store/nodeBalancer/nodeBalancer.requests'; interface UseReduxPreload { _loading: boolean; } -export type ReduxEntity = 'linodes' | 'nodeBalancers' | 'events' | 'longview'; +export type ReduxEntity = 'linodes' | 'events' | 'longview'; type RequestMap = Record; const requestMap: RequestMap = { linodes: () => requestLinodes({}), - nodeBalancers: getAllNodeBalancers, events: getEvents, longview: getAllLongviewClients, }; diff --git a/packages/manager/src/mocks/testBrowser.ts b/packages/manager/src/mocks/testBrowser.ts index 6b95b46f170..fbce424e014 100644 --- a/packages/manager/src/mocks/testBrowser.ts +++ b/packages/manager/src/mocks/testBrowser.ts @@ -4,7 +4,6 @@ import { ENABLE_DEV_TOOLS, isProductionBuild } from 'src/constants'; import { MockData, mockDataController } from 'src/dev-tools/mockDataController'; import store, { ApplicationState } from 'src/store'; import { requestLinodes } from 'src/store/linodes/linode.requests'; -import { getAllNodeBalancers } from 'src/store/nodeBalancer/nodeBalancer.requests'; import { handlers, mockDataHandlers } from './serverHandlers'; let worker: SetupWorkerApi; @@ -33,9 +32,6 @@ const requestEntities = (mockData: MockData, reduxState: ApplicationState) => { if (mockData.linode && !reduxState.__resources.linodes.loading) { store.dispatch(requestLinodes({}) as any); } - if (mockData.nodeBalancer && !reduxState.__resources.nodeBalancers.loading) { - store.dispatch(getAllNodeBalancers() as any); - } }; export { worker }; diff --git a/packages/manager/src/queries/base.ts b/packages/manager/src/queries/base.ts index 9a436e2f014..8b8dfddf5ee 100644 --- a/packages/manager/src/queries/base.ts +++ b/packages/manager/src/queries/base.ts @@ -131,7 +131,7 @@ export const itemInListMutationHandler = < V, E = APIError[] >( - queryKey: string + queryKey: QueryKey ): UseMutationOptions void> => { return { onSuccess: (updatedEntity, variables) => { diff --git a/packages/manager/src/queries/nodebalancers.ts b/packages/manager/src/queries/nodebalancers.ts index c228cd98abe..9d47b427fdc 100644 --- a/packages/manager/src/queries/nodebalancers.ts +++ b/packages/manager/src/queries/nodebalancers.ts @@ -1,13 +1,39 @@ +import { DateTime } from 'luxon'; +import { useInfiniteQuery, useMutation, useQuery } from 'react-query'; +import { parseAPIDate } from 'src/utilities/date'; +import { getAll } from 'src/utilities/getAll'; import { + createNodeBalancer, + createNodeBalancerConfig, + CreateNodeBalancerConfig, + CreateNodeBalancerPayload, + deleteNodeBalancer, + deleteNodeBalancerConfig, + getNodeBalancer, + getNodeBalancerConfigs, + getNodeBalancers, getNodeBalancerStats, + NodeBalancer, + NodeBalancerConfig, NodeBalancerStats, + updateNodeBalancer, + updateNodeBalancerConfig, } from '@linode/api-v4/lib/nodebalancers'; -import { APIError } from '@linode/api-v4/lib/types'; -import { DateTime } from 'luxon'; -import { useQuery } from 'react-query'; -import { parseAPIDate } from 'src/utilities/date'; +import { + APIError, + Filter, + Params, + ResourcePage, +} from '@linode/api-v4/lib/types'; +import { + itemInListCreationHandler, + itemInListMutationHandler, + queryClient, + updateInPaginatedStore, +} from './base'; const queryKey = 'nodebalancers'; + export const NODEBALANCER_STATS_NOT_READY_API_MESSAGE = 'Stats are unavailable at this time.'; @@ -21,7 +47,7 @@ const getIsTooEarlyForStats = (created?: string) => { export const useNodeBalancerStats = (id: number, created?: string) => { return useQuery( - [`${queryKey}-stats`, id], + [queryKey, id, 'stats'], getIsTooEarlyForStats(created) ? () => Promise.reject([{ reason: NODEBALANCER_STATS_NOT_READY_API_MESSAGE }]) @@ -33,3 +59,117 @@ export const useNodeBalancerStats = (id: number, created?: string) => { { refetchInterval: 20000, retry: false } ); }; + +export const useNodeBalancersQuery = (params: Params, filter: Filter) => + useQuery, APIError[]>( + [queryKey, 'list', params, filter], + () => getNodeBalancers(params, filter), + { keepPreviousData: true } + ); + +export const useNodeBalancerQuery = (id: number, enabled = true) => + useQuery( + [queryKey, id], + () => getNodeBalancer(id), + { enabled } + ); + +export const useNodebalancerUpdateMutation = (id: number) => + useMutation>( + (data) => updateNodeBalancer(id, data), + { + onSuccess(data) { + queryClient.setQueryData([queryKey, id], data); + updateInPaginatedStore([queryKey, 'list'], id, data); + }, + } + ); + +export const useNodebalancerDeleteMutation = (id: number) => + useMutation<{}, APIError[]>(() => deleteNodeBalancer(id), { + onSuccess() { + queryClient.invalidateQueries([queryKey, 'list']); + queryClient.removeQueries([queryKey, id]); + }, + }); + +export const useNodebalancerCreateMutation = () => + useMutation( + createNodeBalancer, + { + onSuccess(data) { + queryClient.invalidateQueries([queryKey, 'list']); + queryClient.setQueryData([queryKey, data.id], data); + }, + } + ); + +export const useNodebalancerConfigCreateMutation = (id: number) => + useMutation( + (data) => createNodeBalancerConfig(id, data), + itemInListCreationHandler([queryKey, id, 'configs']) + ); + +export const useNodebalancerConfigUpdateMutation = (nodebalancerId: number) => + useMutation< + NodeBalancerConfig, + APIError[], + Partial & { configId: number } + >( + ({ configId, ...data }) => + updateNodeBalancerConfig(nodebalancerId, configId, data), + itemInListMutationHandler([queryKey, nodebalancerId, 'configs']) + ); + +export const useNodebalancerConfigDeleteMutation = (nodebalancerId: number) => + useMutation<{}, APIError[], { configId: number }>( + ({ configId }) => deleteNodeBalancerConfig(nodebalancerId, configId), + { + onSuccess(_, vars) { + queryClient.setQueryData( + [queryKey, nodebalancerId, 'configs'], + (oldData) => { + return (oldData ?? []).filter( + (config) => config.id !== vars.configId + ); + } + ); + }, + } + ); + +export const useAllNodeBalancerConfigsQuery = (id: number) => + useQuery([queryKey, id, 'configs'], () => + getAllNodeBalancerConfigs(id) + ); + +export const getAllNodeBalancerConfigs = (id: number) => + getAll((params) => + getNodeBalancerConfigs(id, params) + )().then((data) => data.data); + +export const getAllNodeBalancers = () => + getAll((params) => getNodeBalancers(params))().then( + (data) => data.data + ); + +// Please don't use +export const useAllNodeBalancersQuery = (enabled = true) => + useQuery([queryKey, 'all'], getAllNodeBalancers, { + enabled, + }); + +export const useInfiniteNodebalancersQuery = (filter: Filter) => + useInfiniteQuery, APIError[]>( + [queryKey, filter], + ({ pageParam }) => + getNodeBalancers({ page: pageParam, page_size: 25 }, filter), + { + getNextPageParam: ({ page, pages }) => { + if (page === pages) { + return undefined; + } + return page + 1; + }, + } + ); diff --git a/packages/manager/src/queries/volumes.ts b/packages/manager/src/queries/volumes.ts index a8d06c25ade..05d9d9ad33a 100644 --- a/packages/manager/src/queries/volumes.ts +++ b/packages/manager/src/queries/volumes.ts @@ -47,7 +47,7 @@ export const useVolumesQuery = (params: Params, filters: Filter) => { keepPreviousData: true } ); -export const useInfiniteVolumesQuery = (filter: any) => +export const useInfiniteVolumesQuery = (filter: Filter) => useInfiniteQuery, APIError[]>( [queryKey, filter], ({ pageParam }) => getVolumes({ page: pageParam, page_size: 25 }, filter), diff --git a/packages/manager/src/store/index.ts b/packages/manager/src/store/index.ts index 66997f1f2c9..365a0ed2a06 100644 --- a/packages/manager/src/store/index.ts +++ b/packages/manager/src/store/index.ts @@ -55,14 +55,6 @@ import longviewStats, { defaultState as defaultLongviewStatsState, State as LongviewStatsState, } from 'src/store/longviewStats/longviewStats.reducer'; -import nodeBalancers, { - defaultState as defaultNodeBalancerState, - State as NodeBalancersState, -} from 'src/store/nodeBalancer/nodeBalancer.reducer'; -import nodeBalancerConfigs, { - defaultState as defaultNodeBalancerConfigState, - State as NodeBalancerConfigsState, -} from 'src/store/nodeBalancerConfig/nodeBalancerConfig.reducer'; import stackScriptDialog, { defaultState as stackScriptDialogDefaultState, State as StackScriptDialogState, @@ -89,8 +81,6 @@ import mockFeatureFlags, { defaultMockFeatureFlagState, MockFeatureFlagState, } from './mockFeatureFlags'; -import nodeBalancerEvents from './nodeBalancer/nodeBalancer.events'; -import nodeBalancerConfigEvents from './nodeBalancerConfig/nodeBalancerConfig.events'; import pendingUpload, { defaultState as pendingUploadState, State as PendingUploadState, @@ -108,8 +98,6 @@ const __resourcesDefaultState = { linodes: defaultLinodesState, linodeConfigs: defaultLinodeConfigsState, linodeDisks: defaultLinodeDisksState, - nodeBalancerConfigs: defaultNodeBalancerConfigState, - nodeBalancers: defaultNodeBalancerState, }; export interface ResourcesState { @@ -117,8 +105,6 @@ export interface ResourcesState { linodes: LinodesState; linodeConfigs: LinodeConfigsState; linodeDisks: LinodeDisksState; - nodeBalancerConfigs: NodeBalancerConfigsState; - nodeBalancers: NodeBalancersState; } export interface ApplicationState { @@ -169,8 +155,6 @@ const __resources = combineReducers({ linodes, linodeConfigs, linodeDisks, - nodeBalancers, - nodeBalancerConfigs, }); const reducers = combineReducers({ @@ -199,8 +183,6 @@ const enhancers = compose( combineEventsMiddleware( linodeEvents, longviewEvents, - nodeBalancerEvents, - nodeBalancerConfigEvents, diskEvents, linodeConfigEvents ) diff --git a/packages/manager/src/store/nodeBalancer/nodeBalancer.actions.ts b/packages/manager/src/store/nodeBalancer/nodeBalancer.actions.ts deleted file mode 100644 index 4999ad0f4a7..00000000000 --- a/packages/manager/src/store/nodeBalancer/nodeBalancer.actions.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - CreateNodeBalancerPayload, - NodeBalancer, -} from '@linode/api-v4/lib/nodebalancers'; -import { - APIError, - Filter, - Params, - ResourcePage, -} from '@linode/api-v4/lib/types'; -import { actionCreatorFactory } from 'typescript-fsa'; - -import { GetAllData } from 'src/utilities/getAll'; - -const actionCreator = actionCreatorFactory(`@@manager/nodeBalancer`); - -export interface BalancerParams { - nodeBalancerId: number; -} - -type Entity = NodeBalancer; - -export const getAllNodeBalancersActions = actionCreator.async< - void, - GetAllData, - APIError[] ->(`get-all`); - -export type CreateNodeBalancerParams = CreateNodeBalancerPayload; - -export const createNodeBalancersActions = actionCreator.async< - CreateNodeBalancerParams, - Entity, - APIError[] ->(`create`); - -/** We require the ID in the action, if we just did Partial that makes id number | undefined */ -export type UpdateNodeBalancerParams = BalancerParams & Partial; - -export const updateNodeBalancersActions = actionCreator.async< - UpdateNodeBalancerParams, - Entity, - APIError[] ->(`update`); - -export type DeleteNodeBalancerParams = BalancerParams; - -export const deleteNodeBalancerActions = actionCreator.async< - DeleteNodeBalancerParams, - {}, - APIError[] ->(`delete`); - -export type GetNodeBalancerWithConfigsParams = BalancerParams; - -export const getNodeBalancerWithConfigsActions = actionCreator.async< - GetNodeBalancerWithConfigsParams, - NodeBalancer, - APIError[] ->(`get`); - -/** - * Request a single page of NBs (for Dashboard and similar). - * Does *not* include requests for the configs attached to these - * NBs - */ -export const getNodeBalancersPageActions = actionCreator.async< - { params?: Params; filters?: Filter }, - ResourcePage, - APIError[] ->('get-page'); diff --git a/packages/manager/src/store/nodeBalancer/nodeBalancer.containers.ts b/packages/manager/src/store/nodeBalancer/nodeBalancer.containers.ts deleted file mode 100644 index e0a6e0f3c56..00000000000 --- a/packages/manager/src/store/nodeBalancer/nodeBalancer.containers.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Filter, Params } from '@linode/api-v4'; -import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; -import { connect } from 'react-redux'; -import { Action, bindActionCreators } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { ApplicationState } from 'src/store'; -import { - CreateNodeBalancerParams, - DeleteNodeBalancerParams, -} from 'src/store/nodeBalancer/nodeBalancer.actions'; -import { - getAllNodeBalancers, - getAllNodeBalancersWithConfigs, - getNodeBalancersPage, - getNodeBalancerWithConfigs, -} from 'src/store/nodeBalancer/nodeBalancer.requests'; -import { UpdateNodeBalancerParams } from './nodeBalancer.actions'; -import { - createNodeBalancer, - deleteNodeBalancer, - updateNodeBalancer, -} from './nodeBalancer.requests'; - -export interface WithNodeBalancerActions { - nodeBalancerActions: { - getAllNodeBalancersWithConfigs: () => Promise; - getNodeBalancerWithConfigs: (nodeBalancerID: number) => Promise; - getNodeBalancerPage: (params?: Params, filters?: Filter) => Promise; - getAllNodeBalancers: () => Promise; - createNodeBalancer: ( - params: CreateNodeBalancerParams - ) => Promise; - deleteNodeBalancer: (params: DeleteNodeBalancerParams) => Promise<{}>; - updateNodeBalancer: ( - params: UpdateNodeBalancerParams - ) => Promise; - }; -} - -export const withNodeBalancerActions = connect( - undefined, - (dispatch: ThunkDispatch>) => ({ - nodeBalancerActions: { - ...bindActionCreators( - { - getAllNodeBalancersWithConfigs, - getAllNodeBalancers, - createNodeBalancer, - deleteNodeBalancer, - updateNodeBalancer, - }, - dispatch - ), - getNodeBalancerPage: (params: Params = {}, filters: Filter = {}) => - dispatch(getNodeBalancersPage({ params, filters })), - getNodeBalancerWithConfigs: (nodeBalancerId: number) => - dispatch(getNodeBalancerWithConfigs({ nodeBalancerId })), - }, - }) -); diff --git a/packages/manager/src/store/nodeBalancer/nodeBalancer.events.ts b/packages/manager/src/store/nodeBalancer/nodeBalancer.events.ts deleted file mode 100644 index 0a48684e456..00000000000 --- a/packages/manager/src/store/nodeBalancer/nodeBalancer.events.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { EventAction, EventStatus } from '@linode/api-v4/lib/account'; -import { EventHandler } from 'src/store/types'; -import { ApplicationState } from '..'; -import { removeNodeBalancerConfigs } from '../nodeBalancerConfig/nodeBalancerConfig.actions'; -import { ThunkDispatch } from '../types'; -import { deleteNodeBalancerActions } from './nodeBalancer.actions'; -import { getNodeBalancerWithConfigs } from './nodeBalancer.requests'; - -const nodeBalancerEventHandler: EventHandler = (event, dispatch, getState) => { - const { action, entity, status } = event; - const { id } = entity; - const { __resources } = getState(); - - switch (action) { - case 'nodebalancer_create': - return handleNodeBalancerCreate( - dispatch, - id, - action, - status, - __resources - ); - - case 'nodebalancer_delete': - return handleNodeBalancerDelete( - dispatch, - id, - action, - status, - __resources - ); - } -}; - -export default nodeBalancerEventHandler; - -type NodeBalancerActionHandler = ( - dispatch: ThunkDispatch, - id: number, - action: EventAction, - status: EventStatus, - resourceState: ApplicationState['__resources'] -) => void; - -const handleNodeBalancerCreate: NodeBalancerActionHandler = ( - dispatch, - nodeBalancerId, - action, - status, - resourceState -) => { - switch (status) { - case 'failed': - return; - - case 'finished': - case 'scheduled': - case 'started': - case 'notification': - default: - const { - nodeBalancers: { itemsById: nodeBalancers }, - } = resourceState; - - /** If we already have it, don't request it. */ - if (nodeBalancers[nodeBalancerId]) { - return; - } - - dispatch(getNodeBalancerWithConfigs({ nodeBalancerId })); - return; - } -}; - -const handleNodeBalancerDelete: NodeBalancerActionHandler = ( - dispatch, - nodeBalancerId, - action, - status, - resourceState -) => { - switch (status) { - case 'failed': - case 'finished': - case 'notification': - case 'scheduled': - case 'started': - default: - /** Delete NodeBalancer and all configs owned by the NodeBalancer. */ - const { - nodeBalancers: { itemsById: nodeBalancers }, - nodeBalancerConfigs: { itemsById: nodeBalancerConfigs }, - } = resourceState; - - /** If it's already out of state, don't bother trying to delete. */ - if (!nodeBalancers[nodeBalancerId]) { - return; - } - - const configsArray = Object.values(nodeBalancerConfigs) || []; - - const configsToDelete = configsArray.reduce( - (result, { nodebalancer_id, id }) => - nodebalancer_id === nodeBalancerId ? [...result, id] : result, - [] - ); - - dispatch( - removeNodeBalancerConfigs({ - configIDs: configsToDelete, - nodeBalancerId, - }) - ); - dispatch( - deleteNodeBalancerActions.done({ - params: { nodeBalancerId }, - result: {}, - }) - ); - return; - } -}; diff --git a/packages/manager/src/store/nodeBalancer/nodeBalancer.reducer.test.ts b/packages/manager/src/store/nodeBalancer/nodeBalancer.reducer.test.ts deleted file mode 100644 index 1417926c0f8..00000000000 --- a/packages/manager/src/store/nodeBalancer/nodeBalancer.reducer.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { nodeBalancerFactory } from 'src/factories/nodebalancer'; -import * as actions from './nodeBalancer.actions'; -import reducer, { defaultState } from './nodeBalancer.reducer'; - -const mockNodeBalancers = nodeBalancerFactory.buildList(10); - -const resultsPage = { - page: 1, - pages: 3, - results: 100, - data: mockNodeBalancers, -}; - -const resultsPageSmall = { - ...resultsPage, - pages: 1, - results: mockNodeBalancers.length, -}; - -const getAllResults = { - data: mockNodeBalancers, - results: mockNodeBalancers.length, -}; - -const mockError = [{ reason: 'An error occurred.' }]; - -const addEntities = () => { - return reducer( - defaultState, - actions.getAllNodeBalancersActions.done({ result: getAllResults }) - ); -}; - -describe('NodeBalancers Redux store', () => { - describe('Reducer', () => { - it('should handle a getPage.started request', () => { - const newState = reducer( - defaultState, - actions.getNodeBalancersPageActions.started({}) - ); - expect(newState).toHaveProperty('loading', true); - expect(newState).toHaveProperty('error', {}); - }); - - it('should handle a getPage.done request', () => { - const newState = reducer( - defaultState, - actions.getNodeBalancersPageActions.done({ - params: {}, - result: resultsPage, - }) - ); - expect(newState).toHaveProperty('loading', false); - expect(newState).toHaveProperty('error', {}); - expect(newState).toHaveProperty('results', resultsPage.results); - expect(newState.lastUpdated).toBe(0); - expect(newState.itemsById).toHaveProperty( - String(mockNodeBalancers[0].id) - ); - expect(Object.values(newState.itemsById)).toEqual(mockNodeBalancers); - }); - - it('getPage.done should set lastUpdated if results are equal to total entities available', () => { - const newState = reducer( - defaultState, - actions.getNodeBalancersPageActions.done({ - params: {}, - result: resultsPageSmall, - }) - ); - expect(newState).toHaveProperty('results', resultsPageSmall.results); - expect(newState.lastUpdated).toBeGreaterThan(0); - expect(Object.values(newState.itemsById)).toEqual(mockNodeBalancers); - }); - - it('should handle a getPage.failed request', () => { - const newState = reducer( - defaultState, - actions.getNodeBalancersPageActions.failed({ - params: {}, - error: mockError, - }) - ); - expect(newState).toHaveProperty('loading', false); - expect(newState).toHaveProperty('error', { read: mockError }); - expect(newState.lastUpdated).toBe(0); - }); - - it('should handle a getAllNodeBalancers.start action', () => { - const newState = reducer( - defaultState, - actions.getAllNodeBalancersActions.started() - ); - expect(newState).toHaveProperty('loading', true); - expect(newState).toHaveProperty('error', {}); - expect(newState).toHaveProperty('lastUpdated', 0); - }); - - it('should handle a getAllNodeBalancers.done action', () => { - const newState = reducer( - defaultState, - actions.getAllNodeBalancersActions.done({ result: getAllResults }) - ); - expect(newState).toHaveProperty('loading', false); - expect(newState).toHaveProperty('error', {}); - expect(newState.lastUpdated).toBeGreaterThan(0); - expect(Object.values(newState.itemsById)).toEqual(mockNodeBalancers); - expect(newState).toHaveProperty('results', mockNodeBalancers.length); - }); - - it('should handle a getAllNodeBalancers.failed request', () => { - const newState = reducer( - defaultState, - actions.getAllNodeBalancersActions.failed({ - error: mockError, - }) - ); - expect(newState).toHaveProperty('loading', false); - expect(newState).toHaveProperty('error', { read: mockError }); - expect(newState.lastUpdated).toBe(0); - }); - - it('should handle a createNodeBalancer.started action', () => { - const newState = reducer( - { ...defaultState, error: { create: mockError } }, - actions.createNodeBalancersActions.started({ configs: [] }) - ); - expect(newState).toEqual(defaultState); - }); - - it('should handle a createNodeBalancer.done action', () => { - const newBalancer = nodeBalancerFactory.build(); - const newState = reducer( - defaultState, - actions.createNodeBalancersActions.done({ - result: newBalancer, - params: { configs: [] }, - }) - ); - expect(newState.itemsById).toHaveProperty( - String(newBalancer.id), - newBalancer - ); - expect(newState.error.create).toBeUndefined(); - }); - - it('should handle a createNodeBalancer.failed action', () => { - const newState = reducer( - defaultState, - actions.createNodeBalancersActions.failed({ - error: mockError, - params: { configs: [] }, - }) - ); - expect(newState.error.create).toEqual(mockError); - }); - - it('should handle an updateNodeBalancer.started action', () => { - const newState = reducer( - { ...defaultState, error: { update: mockError } }, - actions.updateNodeBalancersActions.started({ nodeBalancerId: 1 }) - ); - expect(newState.error.update).toBeUndefined(); - }); - - it('should handle an updateNodeBalancer.done action', () => { - const withEntities = addEntities(); - const updatedBalancer = { - ...mockNodeBalancers[1], - label: 'updated-label', - }; - const newState = reducer( - withEntities, - actions.updateNodeBalancersActions.done({ - result: updatedBalancer, - params: { nodeBalancerId: updatedBalancer.id }, - }) - ); - expect(newState.itemsById).toHaveProperty( - String(updatedBalancer.id), - updatedBalancer - ); - expect(newState.error.update).toBeUndefined(); - }); - - it('should handle an updateNodeBalancer.failed action', () => { - const newState = reducer( - defaultState, - actions.updateNodeBalancersActions.failed({ - error: mockError, - params: { nodeBalancerId: 1 }, - }) - ); - expect(newState.error.update).toEqual(mockError); - }); - - it('should handle a delete.started action', () => { - const newState = reducer( - { ...defaultState, error: { delete: mockError } }, - actions.deleteNodeBalancerActions.started({ nodeBalancerId: 1 }) - ); - expect(newState.error.delete).toBeUndefined(); - }); - - it('should handle a delete.done action', () => { - const withEntities = addEntities(); - const newState = reducer( - withEntities, - actions.deleteNodeBalancerActions.done({ - result: {}, - params: { nodeBalancerId: mockNodeBalancers[5].id }, - }) - ); - expect(newState.itemsById).not.toHaveProperty( - String(mockNodeBalancers[5].id) - ); - expect(newState).toHaveProperty('results', mockNodeBalancers.length - 1); - expect(newState.error.delete).toBeUndefined(); - }); - - it('should handle a delete.failed action', () => { - const newState = reducer( - defaultState, - actions.deleteNodeBalancerActions.failed({ - error: mockError, - params: { nodeBalancerId: 1 }, - }) - ); - expect(newState.error.delete).toEqual(mockError); - }); - - it('should handle a requestOne.done action when the requested NB is not already in the store', () => { - const mockNodeBalancer = nodeBalancerFactory.build(); - const newState = reducer( - defaultState, - actions.getNodeBalancerWithConfigsActions.done({ - params: { nodeBalancerId: mockNodeBalancer.id }, - result: mockNodeBalancer, - }) - ); - expect(newState.itemsById).toHaveProperty( - String(mockNodeBalancer.id), - mockNodeBalancer - ); - expect(newState.results).toBe(1); - }); - - it('should handle a requestOne.done action when the requested NB is already in the store', () => { - const withEntities = addEntities(); - const updatedBalancer = { - ...mockNodeBalancers[1], - label: 'updated-label', - }; - const newState = reducer( - withEntities, - actions.getNodeBalancerWithConfigsActions.done({ - params: { nodeBalancerId: updatedBalancer.id }, - result: updatedBalancer, - }) - ); - expect(newState.itemsById).toHaveProperty( - String(updatedBalancer.id), - updatedBalancer - ); - expect(newState.results).toBe(mockNodeBalancers.length); - }); - }); -}); diff --git a/packages/manager/src/store/nodeBalancer/nodeBalancer.reducer.ts b/packages/manager/src/store/nodeBalancer/nodeBalancer.reducer.ts deleted file mode 100644 index 403bb6fec05..00000000000 --- a/packages/manager/src/store/nodeBalancer/nodeBalancer.reducer.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; -import { Reducer } from 'redux'; -import { - EntityError, - MappedEntityState2 as MappedEntityState, -} from 'src/store/types'; -import { isType } from 'typescript-fsa'; -import { - createDefaultState, - onCreateOrUpdate, - onDeleteSuccess, - onError, - onGetAllSuccess, - onGetPageSuccess, - onStart, -} from '../store.helpers.tmp'; -import { - createNodeBalancersActions, - deleteNodeBalancerActions, - getAllNodeBalancersActions, - getNodeBalancersPageActions, - getNodeBalancerWithConfigsActions, - updateNodeBalancersActions, -} from './nodeBalancer.actions'; - -export type State = MappedEntityState; - -export const defaultState: State = createDefaultState({}, {}); - -const reducer: Reducer = (state = defaultState, action) => { - /** Get all */ - if (isType(action, getAllNodeBalancersActions.started)) { - return onStart(state); - } - - if (isType(action, getAllNodeBalancersActions.done)) { - const { result } = action.payload; - - if (result.data.length === 0) { - return { - ...state, - loading: false, - lastUpdated: Date.now(), - }; - } - - return onGetAllSuccess(result.data, state, result.results); - } - - if (isType(action, getAllNodeBalancersActions.failed)) { - const { error } = action.payload; - return onError, EntityError>( - { - read: error, - }, - state - ); - } - - /** Create */ - - if (isType(action, createNodeBalancersActions.started)) { - return onError({ create: undefined }, state); - } - - if (isType(action, createNodeBalancersActions.done)) { - const { result } = action.payload; - - return onCreateOrUpdate(result, state); - } - - if (isType(action, createNodeBalancersActions.failed)) { - const { error } = action.payload; - - return onError({ create: error }, state); - } - - /** Update */ - - if (isType(action, updateNodeBalancersActions.started)) { - return onError({ update: undefined }, state); - } - - if (isType(action, updateNodeBalancersActions.done)) { - const { result } = action.payload; - - return onCreateOrUpdate(result, state); - } - - if (isType(action, updateNodeBalancersActions.failed)) { - const { error } = action.payload; - - return onError({ update: error }, state); - } - - /** Delete */ - - if (isType(action, deleteNodeBalancerActions.started)) { - return onError({ delete: undefined }, state); - } - - if (isType(action, deleteNodeBalancerActions.done)) { - const { - params: { nodeBalancerId }, - } = action.payload; - - return onDeleteSuccess(nodeBalancerId, state); - } - - if (isType(action, deleteNodeBalancerActions.failed)) { - const { error } = action.payload; - - return onError({ delete: error }, state); - } - - /** Add */ - if (isType(action, getNodeBalancerWithConfigsActions.done)) { - const { result } = action.payload; - - return onCreateOrUpdate(result, state); - } - - /** Get single page */ - - if (isType(action, getNodeBalancersPageActions.started)) { - return onStart(state); - } - - if (isType(action, getNodeBalancersPageActions.done)) { - const { result } = action.payload; - - return onGetPageSuccess(result.data, state, result.results); - } - - if (isType(action, getNodeBalancersPageActions.failed)) { - const { error } = action.payload; - - return onError({ read: error }, state); - } - - return state; -}; - -export default reducer; diff --git a/packages/manager/src/store/nodeBalancer/nodeBalancer.requests.ts b/packages/manager/src/store/nodeBalancer/nodeBalancer.requests.ts deleted file mode 100644 index fde4d06e506..00000000000 --- a/packages/manager/src/store/nodeBalancer/nodeBalancer.requests.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - createNodeBalancer as _createNodeBalancer, - deleteNodeBalancer as _deleteNodeBalancer, - getNodeBalancer as _getNodeBalancer, - getNodeBalancers, - NodeBalancer, - updateNodeBalancer as _updateNodeBalancer, -} from '@linode/api-v4/lib/nodebalancers'; -import { getAll } from 'src/utilities/getAll'; -import { getAllNodeBalancerConfigs } from '../nodeBalancerConfig/nodeBalancerConfig.requests'; -import { createRequestThunk } from '../store.helpers.tmp'; -import { ThunkActionCreator } from '../types'; -import { - CreateNodeBalancerParams, - createNodeBalancersActions, - deleteNodeBalancerActions, - getAllNodeBalancersActions, - getNodeBalancersPageActions, - getNodeBalancerWithConfigsActions, - GetNodeBalancerWithConfigsParams, - updateNodeBalancersActions, -} from './nodeBalancer.actions'; - -const getAllNodeBalancersRequest = getAll(getNodeBalancers); - -export const getAllNodeBalancers = createRequestThunk( - getAllNodeBalancersActions, - () => getAllNodeBalancersRequest() -); - -export const getNodeBalancersPage = createRequestThunk( - getNodeBalancersPageActions, - ({ params, filters }) => getNodeBalancers(params, filters) -); - -/** - * API allows for creating of NodeBalancer with configs and nodes, however the response does not - * include the newly created configs or nodes. In order to keep the state updated, we manually - * request the configs after successful creation. - */ -export const createNodeBalancer: ThunkActionCreator< - Promise, - CreateNodeBalancerParams -> = (params) => (dispatch) => { - const { started, done, failed } = createNodeBalancersActions; - - dispatch(started(params)); - - return _createNodeBalancer(params) - .then((response) => { - const { id: nodeBalancerId } = response; - - dispatch(getAllNodeBalancerConfigs({ nodeBalancerId })); - - dispatch(done({ result: response, params })); - return response; - }) - .catch((error) => { - dispatch(failed({ error, params })); - return Promise.reject(error); - }); -}; - -export const deleteNodeBalancer: ThunkActionCreator< - Promise<{}>, - { nodeBalancerId: number } -> = (params) => (dispatch) => { - const { nodeBalancerId } = params; - const { started, done, failed } = deleteNodeBalancerActions; - - dispatch(started({ nodeBalancerId })); - - return _deleteNodeBalancer(nodeBalancerId) - .then((response) => { - dispatch(done({ params, result: {} })); - return response; - }) - .catch((error) => { - dispatch(failed({ params, error })); - return Promise.reject(error); - }); -}; - -export const updateNodeBalancer = createRequestThunk( - updateNodeBalancersActions, - ({ nodeBalancerId, ...data }) => _updateNodeBalancer(nodeBalancerId, data) -); - -export const getAllNodeBalancersWithConfigs: ThunkActionCreator< - Promise -> = () => async (dispatch) => { - const { started, done, failed } = getAllNodeBalancersActions; - dispatch(started()); - - try { - const result = await getAllNodeBalancersRequest(); - result.data.forEach((thisBalancer) => { - dispatch(getAllNodeBalancerConfigs({ nodeBalancerId: thisBalancer.id })) - // Errors will be added to the Redux state for each individual set of configs - .catch((_) => null); - }); - - dispatch(done({ result })); - } catch (error) { - dispatch(failed({ error })); - } -}; - -export const getNodeBalancerWithConfigs: ThunkActionCreator< - Promise, - GetNodeBalancerWithConfigsParams -> = (params) => (dispatch) => { - const { nodeBalancerId } = params; - const { started, done, failed } = getNodeBalancerWithConfigsActions; - - dispatch(started(params)); - // This can be done in parallel. - dispatch(getAllNodeBalancerConfigs({ nodeBalancerId })).catch((_) => null); - return _getNodeBalancer(nodeBalancerId) - .then((nodeBalancer) => { - dispatch(done({ params, result: nodeBalancer })); - return nodeBalancer; - }) - .catch((error) => { - dispatch(failed({ params, error })); - return error; - }); -}; diff --git a/packages/manager/src/store/nodeBalancer/nodeBalancer.selectors.ts b/packages/manager/src/store/nodeBalancer/nodeBalancer.selectors.ts deleted file mode 100644 index c7cc31fe104..00000000000 --- a/packages/manager/src/store/nodeBalancer/nodeBalancer.selectors.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createSelector } from 'reselect'; -import { ApplicationState } from 'src/store'; - -export const nodeBalancersWithConfigs = createSelector( - [ - (resources: ApplicationState['__resources']) => - resources.nodeBalancers.itemsById, - (resources: ApplicationState['__resources']) => - resources.nodeBalancerConfigs, - ], - (nodeBalancers, nodeBalancerConfigs) => { - return Object.values(nodeBalancers).map((nodeBalancer) => ({ - ...nodeBalancer, - configs: Object.values( - nodeBalancerConfigs[nodeBalancer.id]?.itemsById ?? {} - ), - })); - } -); diff --git a/packages/manager/src/store/nodeBalancerConfig/configNode.actions.ts b/packages/manager/src/store/nodeBalancerConfig/configNode.actions.ts deleted file mode 100644 index ffed13d008a..00000000000 --- a/packages/manager/src/store/nodeBalancerConfig/configNode.actions.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - CreateNodeBalancerConfigNode, - NodeBalancerConfigNode, - UpdateNodeBalancerConfigNode, -} from '@linode/api-v4/lib/nodebalancers'; -import { APIError } from '@linode/api-v4/lib/types'; -import { actionCreatorFactory } from 'typescript-fsa'; -import { GetAllData } from 'src/utilities/getAll'; -import { BalancerParams } from '../nodeBalancer/nodeBalancer.actions'; - -export interface ConfigParams extends BalancerParams { - configId: number; -} - -export interface NodeParams extends ConfigParams { - nodeId: number; -} - -const actionCreator = actionCreatorFactory(`@@manager/nodeBalancerConfigNode`); - -export type GetAllConfigNodesParams = ConfigParams; -export const requestNodeBalancerConfigNodesActions = actionCreator.async< - GetAllConfigNodesParams, - GetAllData, - APIError[] ->(`get-all`); - -export type CreateNodeBalancerConfigNodeParams = ConfigParams & - CreateNodeBalancerConfigNode; -export const createNodeBalancerConfigNodeActions = actionCreator.async< - CreateNodeBalancerConfigNodeParams, - NodeBalancerConfigNode, - APIError[] ->(`create`); - -export type UpdateNodeBalancerConfigNodeParams = NodeParams & - UpdateNodeBalancerConfigNode; -export const updateNodeBalancerConfigNodeActions = actionCreator.async< - UpdateNodeBalancerConfigNodeParams, - NodeBalancerConfigNode, - APIError[] ->(`update`); - -export type DeleteNodeBalancerConfigNodeParams = NodeParams; -export const deleteNodeBalancerConfigNodeActions = actionCreator.async< - DeleteNodeBalancerConfigNodeParams, - {}, - APIError[] ->(`delete`); - -export const removeNodeBalancerConfigNodes = actionCreator( - `remove-many` -); - -export const addNodeBalancerConfigNodes = actionCreator< - NodeBalancerConfigNode[] ->(`add-many`); diff --git a/packages/manager/src/store/nodeBalancerConfig/configNode.reducer.test.ts b/packages/manager/src/store/nodeBalancerConfig/configNode.reducer.test.ts deleted file mode 100644 index f65fcd96f8b..00000000000 --- a/packages/manager/src/store/nodeBalancerConfig/configNode.reducer.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { nodes } from 'src/__data__/nodeBalConfigs'; - -import { - createNodeBalancerConfigNodeActions, - deleteNodeBalancerConfigNodeActions, - requestNodeBalancerConfigNodesActions, - updateNodeBalancerConfigNodeActions, -} from './configNode.actions'; -import reducer, { defaultState } from './configNode.reducer'; - -const mockParams = { - configId: nodes[0].config_id || 0, - nodeBalancerId: nodes[0].nodebalancer_id || 0, -}; -const mockError = [{ reason: 'an error' }]; - -const addEntities = () => - reducer( - defaultState, - requestNodeBalancerConfigNodesActions.done({ - result: { data: nodes, results: nodes.length }, - params: mockParams, - }) - ); - -describe('NB Config Node reducer', () => { - it('should should handle a successful request node action', () => { - const newState = reducer( - defaultState, - requestNodeBalancerConfigNodesActions.done({ - result: { data: nodes, results: nodes.length }, - params: mockParams, - }) - )[mockParams.configId]; - expect(Object.values(newState.itemsById)).toEqual(nodes); - expect(newState.lastUpdated).toBeGreaterThan(0); - expect(newState.loading).toBe(false); - expect(newState.results).toBe(nodes.length); - }); - - it('should handle a failed request action', () => { - const newState = reducer( - defaultState, - requestNodeBalancerConfigNodesActions.failed({ - params: mockParams, - error: mockError, - }) - )[mockParams.configId]; - expect(newState.error).toHaveProperty('read', mockError); - }); - - it('should set loading state when starting a request', () => { - expect( - reducer( - defaultState, - requestNodeBalancerConfigNodesActions.started(mockParams) - )[mockParams.configId] - ).toHaveProperty('loading', true); - }); - - it('should handle a successful creation', () => { - const newState = reducer( - defaultState, - createNodeBalancerConfigNodeActions.done({ - result: nodes[0], - params: { ...mockParams, address: '', label: '' }, - }) - )[mockParams.configId]; - expect(Object.values(newState.itemsById)).toContain(nodes[0]); - expect(newState.results).toBe(1); - }); - - it('should handle a failed creation', () => { - const newState = reducer( - defaultState, - createNodeBalancerConfigNodeActions.failed({ - error: mockError, - params: { ...mockParams, address: '', label: '' }, - }) - ); - expect(newState[mockParams.configId].error!.create).toEqual(mockError); - }); - - it('should handle node deletion', () => { - const withEntities = addEntities(); - const newState = reducer( - withEntities, - deleteNodeBalancerConfigNodeActions.done({ - result: {}, - params: { ...mockParams, nodeId: nodes[0].id }, - }) - )[mockParams.configId]; - expect(newState.results).toBe( - Object.keys(withEntities[mockParams.configId].itemsById).length - 1 - ); - }); - - it('should handle a failed node deletion', () => { - const newState = reducer( - defaultState, - deleteNodeBalancerConfigNodeActions.failed({ - params: { ...mockParams, nodeId: 111 }, - error: mockError, - }) - ); - expect(newState[mockParams.configId].error!.delete).toEqual(mockError); - }); - - it('should handle a successful update', () => { - const withEntities = addEntities(); - const updatedNode = { ...nodes[1], label: 'newlabel' }; - const newState = reducer( - withEntities, - updateNodeBalancerConfigNodeActions.done({ - result: updatedNode, - params: { ...mockParams, nodeId: nodes[1].id }, - }) - )[mockParams.configId]; - expect(Object.values(newState.itemsById)).toHaveLength( - Object.keys(withEntities[mockParams.configId].itemsById).length - ); - expect(newState.itemsById[nodes[1].id]).toEqual(updatedNode); - }); - - it('should handle a failed update', () => { - const newState = reducer( - defaultState, - updateNodeBalancerConfigNodeActions.failed({ - error: mockError, - params: { ...mockParams, nodeId: 123 }, - }) - ); - expect(newState[mockParams.configId].error!.update).toEqual(mockError); - }); -}); diff --git a/packages/manager/src/store/nodeBalancerConfig/configNode.reducer.ts b/packages/manager/src/store/nodeBalancerConfig/configNode.reducer.ts deleted file mode 100644 index f82faddb6f2..00000000000 --- a/packages/manager/src/store/nodeBalancerConfig/configNode.reducer.ts +++ /dev/null @@ -1,132 +0,0 @@ -import produce from 'immer'; -import { NodeBalancerConfigNode } from '@linode/api-v4/lib/nodebalancers'; -import { Reducer } from 'redux'; -import { - ensureInitializedNestedState, - onCreateOrUpdate, - onDeleteSuccess, - onError, - onGetAllSuccess, - onStart, -} from 'src/store/store.helpers.tmp'; -import { EntityError, RelationalMappedEntityState } from 'src/store/types'; -import { isType } from 'typescript-fsa'; -import { - createNodeBalancerConfigNodeActions, - deleteNodeBalancerConfigNodeActions, - requestNodeBalancerConfigNodesActions, - updateNodeBalancerConfigNodeActions, -} from './configNode.actions'; - -/** - * State - */ - -export type State = RelationalMappedEntityState< - NodeBalancerConfigNode, - EntityError ->; - -export const defaultState: State = {}; - -/** - * Reducer - */ -const reducer: Reducer = (state = defaultState, action) => { - return produce(state, (draft) => { - if (isType(action, requestNodeBalancerConfigNodesActions.started)) { - const { configId } = action.payload; - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - draft[configId] = onStart(draft[configId]); - } - - if (isType(action, requestNodeBalancerConfigNodesActions.done)) { - const { result } = action.payload; - const { configId } = action.payload.params; - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - - draft[configId] = onGetAllSuccess( - result.data, - draft[configId], - result.results - ); - } - - if (isType(action, requestNodeBalancerConfigNodesActions.failed)) { - const { error } = action.payload; - const { configId } = action.payload.params; - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - - draft[configId] = onError({ read: error }, draft[configId]); - } - - if (isType(action, createNodeBalancerConfigNodeActions.started)) { - const { configId } = action.payload; - - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - draft[configId].error.create = undefined; - } - - if (isType(action, createNodeBalancerConfigNodeActions.done)) { - const { result } = action.payload; - const { configId } = action.payload.params; - - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - draft[configId] = onCreateOrUpdate(result, draft[configId]); - } - - if (isType(action, createNodeBalancerConfigNodeActions.failed)) { - const { error } = action.payload; - const { configId } = action.payload.params; - - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - draft[configId].error.create = error; - } - - if (isType(action, updateNodeBalancerConfigNodeActions.started)) { - const { configId } = action.payload; - - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - draft[configId].error.update = undefined; - } - - if (isType(action, updateNodeBalancerConfigNodeActions.done)) { - const { result } = action.payload; - const { configId } = action.payload.params; - - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - draft[configId] = onCreateOrUpdate(result, draft[configId]); - } - - if (isType(action, updateNodeBalancerConfigNodeActions.failed)) { - const { error } = action.payload; - const { configId } = action.payload.params; - - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - draft[configId].error.update = error; - } - - if (isType(action, deleteNodeBalancerConfigNodeActions.started)) { - const { configId } = action.payload; - - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - draft[configId].error.delete = undefined; - } - - if (isType(action, deleteNodeBalancerConfigNodeActions.done)) { - const { nodeId, configId } = action.payload.params; - - draft[configId] = onDeleteSuccess(nodeId, draft[configId]); - } - - if (isType(action, deleteNodeBalancerConfigNodeActions.failed)) { - const { error } = action.payload; - const { configId } = action.payload.params; - - draft = ensureInitializedNestedState(draft, configId, { results: 0 }); - draft[configId].error.delete = error; - } - }); -}; - -export default reducer; diff --git a/packages/manager/src/store/nodeBalancerConfig/configNode.requests.ts b/packages/manager/src/store/nodeBalancerConfig/configNode.requests.ts deleted file mode 100644 index 6be6b7ff716..00000000000 --- a/packages/manager/src/store/nodeBalancerConfig/configNode.requests.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - createNodeBalancerConfigNode as _createNode, - deleteNodeBalancerConfigNode as _deleteNode, - getNodeBalancerConfigNodes as _getConfigNodes, - updateNodeBalancerConfigNode as _updateNode, -} from '@linode/api-v4/lib/nodebalancers'; -import { getAll } from 'src/utilities/getAll'; -import { createRequestThunk } from '../store.helpers'; -import { - createNodeBalancerConfigNodeActions, - deleteNodeBalancerConfigNodeActions, - requestNodeBalancerConfigNodesActions, - updateNodeBalancerConfigNodeActions, -} from './configNode.actions'; - -const getNodeBalancerConfigNodes = (nodeBalancerId: number, configId: number) => - getAll(() => _getConfigNodes(nodeBalancerId, configId)); - -export const getAllNodeBalancerConfigs = createRequestThunk( - requestNodeBalancerConfigNodesActions, - ({ nodeBalancerId, configId }) => - getNodeBalancerConfigNodes(nodeBalancerId, configId)().then( - ({ data }) => data - ) -); - -export const createNodeBalancerConfig = createRequestThunk( - createNodeBalancerConfigNodeActions, - ({ nodeBalancerId, configId, ...data }) => - _createNode(nodeBalancerId, configId, data) -); - -export const updateNodeBalancerConfig = createRequestThunk( - updateNodeBalancerConfigNodeActions, - ({ nodeBalancerId, configId, nodeId, ...data }) => - _updateNode(nodeBalancerId, configId, nodeId, data) -); - -export const deleteNodeBalancerConfigNode = createRequestThunk( - deleteNodeBalancerConfigNodeActions, - ({ nodeBalancerId, configId, nodeId }) => - _deleteNode(nodeBalancerId, configId, nodeId) -); diff --git a/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.actions.ts b/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.actions.ts deleted file mode 100644 index 5ebd8c61135..00000000000 --- a/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.actions.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - CreateNodeBalancerConfig, - NodeBalancerConfig, - UpdateNodeBalancerConfig, -} from '@linode/api-v4/lib/nodebalancers'; -import { APIError } from '@linode/api-v4/lib/types'; -import { GetAllData } from 'src/utilities/getAll'; -import { actionCreatorFactory } from 'typescript-fsa'; -import { BalancerParams } from '../nodeBalancer/nodeBalancer.actions'; - -type Entity = NodeBalancerConfig; - -export interface ConfigParams extends BalancerParams { - nodeBalancerConfigId: number; -} - -const actionCreator = actionCreatorFactory(`@@manager/nodeBalancerConfig`); - -export type GetAllNodeBalancerConfigsParams = BalancerParams; -export const getAllNodeBalancerConfigsActions = actionCreator.async< - GetAllNodeBalancerConfigsParams, - GetAllData, - APIError[] ->(`get-all`); - -export type CreateNodeBalancerConfigParams = BalancerParams & - CreateNodeBalancerConfig; -export const createNodeBalancerConfigActions = actionCreator.async< - CreateNodeBalancerConfigParams, - Entity, - APIError[] ->(`create`); - -export type UpdateNodeBalancerConfigParams = ConfigParams & - UpdateNodeBalancerConfig; -export const updateNodeBalancerConfigActions = actionCreator.async< - UpdateNodeBalancerConfigParams, - Entity, - APIError[] ->(`update`); - -export type DeleteNodeBalancerConfigParams = ConfigParams; -export const deleteNodeBalancerConfigActions = actionCreator.async< - DeleteNodeBalancerConfigParams, - {}, - APIError[] ->(`delete`); - -interface RemoveConfigParams { - nodeBalancerId: number; - configIDs: number[]; -} -export const removeNodeBalancerConfigs = actionCreator( - `remove-many` -); - -interface AddConfigParams { - nodeBalancerId: number; - configs: NodeBalancerConfig[]; -} -export const addNodeBalancerConfigs = actionCreator( - `add-many` -); diff --git a/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.containers.ts b/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.containers.ts deleted file mode 100644 index 107a6ed1f31..00000000000 --- a/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.containers.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NodeBalancerConfig } from '@linode/api-v4/lib/nodebalancers'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { - createNodeBalancerConfig, - deleteNodeBalancerConfig, - getAllNodeBalancerConfigs, - updateNodeBalancerConfig, -} from 'src/store/nodeBalancerConfig/nodeBalancerConfig.requests'; -import { - CreateNodeBalancerConfigParams, - DeleteNodeBalancerConfigParams, - GetAllNodeBalancerConfigsParams, - UpdateNodeBalancerConfigParams, -} from '../nodeBalancerConfig/nodeBalancerConfig.actions'; - -export interface WithNodeBalancerConfigActions { - nodeBalancerConfigActions: { - getAllNodeBalancerConfigs: ( - params: GetAllNodeBalancerConfigsParams - ) => Promise; - createNodeBalancerConfig: ( - params: CreateNodeBalancerConfigParams - ) => Promise; - deleteNodeBalancerConfig: ( - params: DeleteNodeBalancerConfigParams - ) => Promise<{}>; - updateNodeBalancerConfig: ( - params: UpdateNodeBalancerConfigParams - ) => Promise; - }; -} - -export const withNodeBalancerConfigActions = connect(undefined, (dispatch) => ({ - nodeBalancerConfigActions: bindActionCreators( - { - getAllNodeBalancerConfigs, - createNodeBalancerConfig, - updateNodeBalancerConfig, - deleteNodeBalancerConfig, - }, - dispatch - ), -})); diff --git a/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.events.ts b/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.events.ts deleted file mode 100644 index eb0f984e08a..00000000000 --- a/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.events.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { EventHandler } from 'src/store/types'; -import { getAllNodeBalancerConfigs } from './nodeBalancerConfig.requests'; - -/** - * The entity for these events is the NB the config belongs to. - * When a create/delete config event comes in, request the updated - * configs for that NB to update our store. - */ -const nodeBalancerConfigEventHandler: EventHandler = (event, dispatch) => { - const { action, entity } = event; - const { id } = entity; - - switch (action) { - case 'nodebalancer_config_delete': - case 'nodebalancer_config_create': - return dispatch(getAllNodeBalancerConfigs({ nodeBalancerId: id })); - default: - return; - } -}; - -export default nodeBalancerConfigEventHandler; diff --git a/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.reducer.ts b/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.reducer.ts deleted file mode 100644 index e8c424f39ac..00000000000 --- a/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.reducer.ts +++ /dev/null @@ -1,168 +0,0 @@ -import produce from 'immer'; -import { NodeBalancerConfig } from '@linode/api-v4/lib/nodebalancers'; -import { Reducer } from 'redux'; -import { EntityError, RelationalMappedEntityState } from 'src/store/types'; -import { isType } from 'typescript-fsa'; -import { deleteNodeBalancerActions } from '../nodeBalancer/nodeBalancer.actions'; -import { - addMany, - ensureInitializedNestedState, - onCreateOrUpdate, - onDeleteSuccess, - onError, - onGetAllSuccess, - onStart, - removeMany, -} from '../store.helpers.tmp'; -import { - addNodeBalancerConfigs, - createNodeBalancerConfigActions, - deleteNodeBalancerConfigActions, - getAllNodeBalancerConfigsActions, - removeNodeBalancerConfigs, - updateNodeBalancerConfigActions, -} from './nodeBalancerConfig.actions'; - -export type State = RelationalMappedEntityState< - NodeBalancerConfig, - EntityError ->; - -export const defaultState: State = {}; - -const reducer: Reducer = (state = defaultState, action) => { - return produce(state, (draft) => { - if (isType(action, getAllNodeBalancerConfigsActions.started)) { - const { nodeBalancerId } = action.payload; - draft = ensureInitializedNestedState(draft, nodeBalancerId); - - draft[nodeBalancerId] = onStart(draft[nodeBalancerId]); - } - - if (isType(action, getAllNodeBalancerConfigsActions.done)) { - const { result } = action.payload; - const { nodeBalancerId } = action.payload.params; - draft = ensureInitializedNestedState(draft, nodeBalancerId); - - draft[nodeBalancerId] = onGetAllSuccess( - result.data, - draft[nodeBalancerId], - result.results - ); - } - - if (isType(action, getAllNodeBalancerConfigsActions.failed)) { - const { error } = action.payload; - const { nodeBalancerId } = action.payload.params; - - draft = ensureInitializedNestedState(draft, nodeBalancerId); - - draft[nodeBalancerId] = onError({ read: error }, draft[nodeBalancerId]); - } - - /** Create */ - - if (isType(action, createNodeBalancerConfigActions.started)) { - const { nodeBalancerId } = action.payload; - - draft = ensureInitializedNestedState(draft, nodeBalancerId); - draft[nodeBalancerId].error.create = undefined; - } - - if (isType(action, createNodeBalancerConfigActions.done)) { - const { result } = action.payload; - const { nodeBalancerId } = action.payload.params; - - draft = ensureInitializedNestedState(draft, nodeBalancerId); - draft[nodeBalancerId] = onCreateOrUpdate(result, draft[nodeBalancerId]); - } - - if (isType(action, createNodeBalancerConfigActions.failed)) { - const { error } = action.payload; - const { nodeBalancerId } = action.payload.params; - - draft = ensureInitializedNestedState(draft, nodeBalancerId); - draft[nodeBalancerId].error.create = error; - } - - /** Update */ - if (isType(action, updateNodeBalancerConfigActions.started)) { - const { nodeBalancerId } = action.payload; - draft = ensureInitializedNestedState(draft, nodeBalancerId); - draft[nodeBalancerId].error.update = undefined; - } - - if (isType(action, updateNodeBalancerConfigActions.done)) { - const { result } = action.payload; - const { nodeBalancerId } = action.payload.params; - draft = ensureInitializedNestedState(draft, nodeBalancerId, { - results: 0, - }); - draft[nodeBalancerId] = onCreateOrUpdate(result, draft[nodeBalancerId]); - } - - if (isType(action, updateNodeBalancerConfigActions.failed)) { - const { error } = action.payload; - const { nodeBalancerId } = action.payload.params; - - draft[nodeBalancerId] = onError({ update: error }, draft[nodeBalancerId]); - } - - /** Delete */ - if (isType(action, deleteNodeBalancerConfigActions.started)) { - const { nodeBalancerId } = action.payload; - - draft = ensureInitializedNestedState(draft, nodeBalancerId); - draft[nodeBalancerId].error.delete = undefined; - } - - if (isType(action, deleteNodeBalancerConfigActions.done)) { - const { nodeBalancerConfigId, nodeBalancerId } = action.payload.params; - - draft[nodeBalancerId] = onDeleteSuccess( - nodeBalancerConfigId, - draft[nodeBalancerId] - ); - } - - if (isType(action, deleteNodeBalancerConfigActions.failed)) { - const { error } = action.payload; - const { nodeBalancerId } = action.payload.params; - - draft = ensureInitializedNestedState(draft, nodeBalancerId, { - results: 0, - }); - draft[nodeBalancerId].error.delete = error; - } - - if (isType(action, removeNodeBalancerConfigs)) { - const { configIDs, nodeBalancerId } = action.payload; - draft = ensureInitializedNestedState(draft, nodeBalancerId, { - results: 0, - }); - draft[nodeBalancerId] = removeMany( - configIDs.map(String), - draft[nodeBalancerId] - ); - } - - if (isType(action, addNodeBalancerConfigs)) { - const { configs, nodeBalancerId } = action.payload; - - draft = ensureInitializedNestedState(draft, nodeBalancerId, { - results: 0, - }); - draft[nodeBalancerId] = addMany(configs, draft[nodeBalancerId]); - } - - /** When a NodeBalancer is deleted, we need to remove all of its configs. */ - if (isType(action, deleteNodeBalancerActions.done)) { - const { - params: { nodeBalancerId }, - } = action.payload; - delete draft[nodeBalancerId]; - } - }); -}; - -export default reducer; diff --git a/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.requests.ts b/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.requests.ts deleted file mode 100644 index 9e28e0f91e0..00000000000 --- a/packages/manager/src/store/nodeBalancerConfig/nodeBalancerConfig.requests.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - createNodeBalancerConfig as _createNodeBalancerConfig, - deleteNodeBalancerConfig as _deleteNodeBalancerConfig, - getNodeBalancerConfigs as _getNodeBalancerConfigs, - NodeBalancerConfig, - updateNodeBalancerConfig as _updateNodeBalancerConfig, -} from '@linode/api-v4/lib/nodebalancers'; -import { getAll } from 'src/utilities/getAll'; -import { createRequestThunk } from '../store.helpers'; -import { ThunkActionCreator } from '../types'; -import { - createNodeBalancerConfigActions, - deleteNodeBalancerConfigActions, - getAllNodeBalancerConfigsActions, - updateNodeBalancerConfigActions, -} from './nodeBalancerConfig.actions'; - -const getNodeBalancerConfigs = (nodeBalancerId: number) => - getAll(() => _getNodeBalancerConfigs(nodeBalancerId)); - -export const getAllNodeBalancerConfigs = createRequestThunk( - getAllNodeBalancerConfigsActions, - ({ nodeBalancerId }) => getNodeBalancerConfigs(nodeBalancerId)() -); - -export const createNodeBalancerConfig = createRequestThunk( - createNodeBalancerConfigActions, - ({ nodeBalancerId, ...data }) => - _createNodeBalancerConfig(nodeBalancerId, data) -); - -export const updateNodeBalancerConfig = createRequestThunk( - updateNodeBalancerConfigActions, - ({ nodeBalancerId, nodeBalancerConfigId, ...data }) => - _updateNodeBalancerConfig(nodeBalancerId, nodeBalancerConfigId, data) -); - -export const deleteNodeBalancerConfig: ThunkActionCreator< - Promise<{}>, - { nodeBalancerConfigId: number; nodeBalancerId: number } -> = (params) => (dispatch) => { - const { nodeBalancerConfigId, nodeBalancerId } = params; - const { started, done, failed } = deleteNodeBalancerConfigActions; - - dispatch(started(params)); - - return _deleteNodeBalancerConfig(nodeBalancerId, nodeBalancerConfigId) - .then((response) => { - dispatch(done({ params, result: response })); - return response; - }) - .catch((error) => { - dispatch(failed({ params, error })); - return Promise.reject(error); - }); -}; diff --git a/packages/manager/src/store/selectors/getSearchEntities.test.ts b/packages/manager/src/store/selectors/getSearchEntities.test.ts deleted file mode 100644 index e29887decf5..00000000000 --- a/packages/manager/src/store/selectors/getSearchEntities.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { assocPath } from 'ramda'; -import types from 'src/utilities/types.json'; -import { kubernetesClusterFactory } from 'src/factories'; -import { nodeBalancerFactory } from 'src/factories/nodebalancer'; -import { apiResponseToMappedState } from 'src/store/store.helpers.tmp'; -import { domains, linodes } from 'src/__data__'; -import getSearchEntities from './getSearchEntities'; - -const nodeBalancers = nodeBalancerFactory.buildList(5); - -describe('getSearchEntities selector', () => { - const mockState: any = { - domains: { itemsById: apiResponseToMappedState(domains) }, - linodes: { - itemsById: apiResponseToMappedState(linodes), - }, - types: { entities: types.data }, - nodeBalancers: { - itemsById: apiResponseToMappedState(nodeBalancers), - }, - kubernetes: { - itemsById: apiResponseToMappedState( - kubernetesClusterFactory.buildList(2) - ), - }, - nodePools: { - entities: [], - }, - buckets: { - data: [], - }, - }; - it('should return an array of SearchableItems', () => { - const results = getSearchEntities(mockState); - expect(results).toBeInstanceOf(Array); - }); - it('should not recompute objects if the list of entities does not change.', () => { - getSearchEntities.resetRecomputations(); - getSearchEntities(mockState); - expect(getSearchEntities.recomputations()).toEqual(0); - }); - it('should recompute objects if the list of entities changes.', () => { - getSearchEntities.resetRecomputations(); - getSearchEntities({ ...mockState, linodes: { itemsById: {} } }); - expect(getSearchEntities.recomputations()).toEqual(1); - }); - it('should recompute if an entry in entities is updated', () => { - getSearchEntities.resetRecomputations(); - const updatedLinodes = assocPath([0, 'label'], 'newlabel', linodes); - getSearchEntities({ - ...mockState, - linodes: { - itemsById: apiResponseToMappedState(updatedLinodes), - }, - }); - expect(getSearchEntities.recomputations()).toEqual(1); - }); -}); diff --git a/packages/manager/src/store/selectors/getSearchEntities.ts b/packages/manager/src/store/selectors/getSearchEntities.ts index f936e3ebea2..50cd8b9ce85 100644 --- a/packages/manager/src/store/selectors/getSearchEntities.ts +++ b/packages/manager/src/store/selectors/getSearchEntities.ts @@ -4,7 +4,6 @@ import { KubernetesCluster } from '@linode/api-v4/lib/kubernetes'; import { Linode } from '@linode/api-v4/lib/linodes'; import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; import { Volume } from '@linode/api-v4/lib/volumes'; -import { createSelector } from 'reselect'; import { displayType } from 'src/features/linodes/presentation'; import { getDescriptionForCluster } from 'src/features/Kubernetes/kubeUtils'; import { SearchableItem } from 'src/features/Search/search.interfaces'; @@ -165,12 +164,3 @@ export const bucketToSearchableItem = ( description: readableBytes(bucket.size).formatted, }, }); - -const nodebalSelector = ({ nodeBalancers }: State) => - Object.values(nodeBalancers.itemsById); - -export default createSelector(nodebalSelector, (nodebalancers) => { - const searchableNodebalancers = nodebalancers.map(nodeBalToSearchableItem); - - return [...searchableNodebalancers]; -}); diff --git a/packages/manager/src/store/selectors/index.ts b/packages/manager/src/store/selectors/index.ts index 7819e34a946..0bb061bcd0a 100644 --- a/packages/manager/src/store/selectors/index.ts +++ b/packages/manager/src/store/selectors/index.ts @@ -3,7 +3,6 @@ import { registerSelectors } from 'reselect-tools'; import entityErrors from './entitiesErrors'; import entitiesLoading from './entitiesLoading'; import getEntitiesWithGroupsToImport from './getEntitiesWithGroupsToImport'; -import getSearchEntities from './getSearchEntities'; import inProgressEventForLinode from './inProgressEventForLinode'; import inProgressEvents from './inProgressEvents'; import recentEventForLinode from './recentEventForLinode'; @@ -17,7 +16,6 @@ export const initReselectDevtools = () => { entityErrors, entitiesLoading, getEntitiesWithGroupsToImport, - getSearchEntities, inProgressEventForLinode, inProgressEvents, recentEventForLinode, diff --git a/packages/manager/src/utilities/getEntityByIDFromStore.ts b/packages/manager/src/utilities/getEntityByIDFromStore.ts index 1c47047f5a4..cfc39c084eb 100644 --- a/packages/manager/src/utilities/getEntityByIDFromStore.ts +++ b/packages/manager/src/utilities/getEntityByIDFromStore.ts @@ -28,12 +28,10 @@ const _getEntityByIDFromStore = ( return; } const _store = store.getState(); - const { linodes, nodeBalancers } = _store.__resources; + const { linodes } = _store.__resources; switch (entityType) { case 'linode': return linodes.itemsById[entityID]; - case 'nodebalancer': - return nodeBalancers.itemsById[entityID]; default: return; } From fb5ec7e324eab8e8093bfd270fe3d87c24440955 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 31 Mar 2023 15:15:11 -0400 Subject: [PATCH 02/18] work on invalidations --- .../NodeBalancers/NodeBalancerCreate.tsx | 13 +++- .../NodeBalancerConfigurations.tsx | 20 ++++++ packages/manager/src/queries/nodebalancers.ts | 64 +++++++++++++------ 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index d3ed2c5c754..516906abc87 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -51,12 +51,13 @@ import { } from './utils'; import { queryClient, simpleMutationHandlers } from 'src/queries/base'; import { - queryKey, + queryKey as accountAgreementsQueryKey, reportAgreementSigningError, } from 'src/queries/accountAgreements'; import LandingHeader from 'src/components/LandingHeader'; import { Region } from '@linode/api-v4/lib/regions'; import { createNodeBalancer } from '@linode/api-v4/lib/nodebalancers'; +import { queryKey } from 'src/queries/nodebalancers'; type ClassNames = 'title' | 'sidebar'; @@ -317,6 +318,12 @@ class NodeBalancerCreate extends React.Component { createNodeBalancer(nodeBalancerRequestData) .then((nodeBalancer) => { + queryClient.invalidateQueries([queryKey]); + queryClient.setQueryData( + [queryKey, 'nodebalancer', nodeBalancer.id], + nodeBalancer + ); + this.props.history.push(`/nodebalancers/${nodeBalancer.id}/summary`); // GA Event sendCreateNodeBalancerEvent( @@ -326,9 +333,9 @@ class NodeBalancerCreate extends React.Component { queryClient.executeMutation<{}, APIError[], Partial>({ variables: { eu_model: true, privacy_policy: true }, mutationFn: signAgreement, - mutationKey: queryKey, + mutationKey: accountAgreementsQueryKey, onError: reportAgreementSigningError, - ...simpleMutationHandlers(queryKey), + ...simpleMutationHandlers(accountAgreementsQueryKey), }); } }) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx index fced1aa47d7..8cd5e82f259 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx @@ -53,6 +53,8 @@ import { parseAddresses, transformConfigsForRequest, } from '../utils'; +import { queryClient } from 'src/queries/base'; +import { queryKey } from 'src/queries/nodebalancers'; type ClassNames = 'title' | 'port' | 'nbStatuses' | 'button'; @@ -305,6 +307,12 @@ class NodeBalancerConfigurations extends React.Component { configPayload ) .then((nodeBalancerConfig) => { + queryClient.invalidateQueries([ + queryKey, + 'nodebalancer', + Number(nodeBalancerId), + 'configs', + ]); // update config data const newConfigs = clone(this.state.configs); newConfigs[idx] = { ...nodeBalancerConfig, nodes: [] }; @@ -421,6 +429,12 @@ class NodeBalancerConfigurations extends React.Component { createNodeBalancerConfig(Number(nodeBalancerId), configPayload) .then((nodeBalancerConfig) => { + queryClient.invalidateQueries([ + queryKey, + 'nodebalancer', + Number(nodeBalancerId), + 'configs', + ]); // update config data const newConfigs = clone(this.state.configs); newConfigs[idx] = { ...nodeBalancerConfig, nodes: [] }; @@ -587,6 +601,12 @@ class NodeBalancerConfigurations extends React.Component { // actually delete a real config deleteNodeBalancerConfig(Number(nodeBalancerId), config.id) .then((_) => { + queryClient.invalidateQueries([ + queryKey, + 'nodebalancer', + Number(nodeBalancerId), + 'configs', + ]); // update config data const newConfigs = clone(this.state.configs); newConfigs.splice(idxToDelete, 1); diff --git a/packages/manager/src/queries/nodebalancers.ts b/packages/manager/src/queries/nodebalancers.ts index 9d47b427fdc..de0a06a919b 100644 --- a/packages/manager/src/queries/nodebalancers.ts +++ b/packages/manager/src/queries/nodebalancers.ts @@ -18,7 +18,8 @@ import { NodeBalancerStats, updateNodeBalancer, updateNodeBalancerConfig, -} from '@linode/api-v4/lib/nodebalancers'; + Event, +} from '@linode/api-v4'; import { APIError, Filter, @@ -29,10 +30,9 @@ import { itemInListCreationHandler, itemInListMutationHandler, queryClient, - updateInPaginatedStore, } from './base'; -const queryKey = 'nodebalancers'; +export const queryKey = 'nodebalancers'; export const NODEBALANCER_STATS_NOT_READY_API_MESSAGE = 'Stats are unavailable at this time.'; @@ -47,7 +47,7 @@ const getIsTooEarlyForStats = (created?: string) => { export const useNodeBalancerStats = (id: number, created?: string) => { return useQuery( - [queryKey, id, 'stats'], + [queryKey, 'nodebalancer', id, 'stats'], getIsTooEarlyForStats(created) ? () => Promise.reject([{ reason: NODEBALANCER_STATS_NOT_READY_API_MESSAGE }]) @@ -62,14 +62,14 @@ export const useNodeBalancerStats = (id: number, created?: string) => { export const useNodeBalancersQuery = (params: Params, filter: Filter) => useQuery, APIError[]>( - [queryKey, 'list', params, filter], + [queryKey, 'paginated', params, filter], () => getNodeBalancers(params, filter), { keepPreviousData: true } ); export const useNodeBalancerQuery = (id: number, enabled = true) => useQuery( - [queryKey, id], + [queryKey, 'nodebalancer', id], () => getNodeBalancer(id), { enabled } ); @@ -79,8 +79,8 @@ export const useNodebalancerUpdateMutation = (id: number) => (data) => updateNodeBalancer(id, data), { onSuccess(data) { - queryClient.setQueryData([queryKey, id], data); - updateInPaginatedStore([queryKey, 'list'], id, data); + queryClient.invalidateQueries([queryKey]); + queryClient.setQueryData([queryKey, 'nodebalancer', id], data); }, } ); @@ -88,8 +88,8 @@ export const useNodebalancerUpdateMutation = (id: number) => export const useNodebalancerDeleteMutation = (id: number) => useMutation<{}, APIError[]>(() => deleteNodeBalancer(id), { onSuccess() { - queryClient.invalidateQueries([queryKey, 'list']); - queryClient.removeQueries([queryKey, id]); + queryClient.removeQueries([queryKey, 'nodebalancer', id]); + queryClient.invalidateQueries([queryKey]); }, }); @@ -98,8 +98,8 @@ export const useNodebalancerCreateMutation = () => createNodeBalancer, { onSuccess(data) { - queryClient.invalidateQueries([queryKey, 'list']); - queryClient.setQueryData([queryKey, data.id], data); + queryClient.invalidateQueries([queryKey]); + queryClient.setQueryData([queryKey, 'nodebalancer', data.id], data); }, } ); @@ -107,7 +107,7 @@ export const useNodebalancerCreateMutation = () => export const useNodebalancerConfigCreateMutation = (id: number) => useMutation( (data) => createNodeBalancerConfig(id, data), - itemInListCreationHandler([queryKey, id, 'configs']) + itemInListCreationHandler([queryKey, 'nodebalancer', id, 'configs']) ); export const useNodebalancerConfigUpdateMutation = (nodebalancerId: number) => @@ -118,7 +118,12 @@ export const useNodebalancerConfigUpdateMutation = (nodebalancerId: number) => >( ({ configId, ...data }) => updateNodeBalancerConfig(nodebalancerId, configId, data), - itemInListMutationHandler([queryKey, nodebalancerId, 'configs']) + itemInListMutationHandler([ + queryKey, + 'nodebalancer', + nodebalancerId, + 'configs', + ]) ); export const useNodebalancerConfigDeleteMutation = (nodebalancerId: number) => @@ -127,7 +132,7 @@ export const useNodebalancerConfigDeleteMutation = (nodebalancerId: number) => { onSuccess(_, vars) { queryClient.setQueryData( - [queryKey, nodebalancerId, 'configs'], + [queryKey, 'nodebalancer', nodebalancerId, 'configs'], (oldData) => { return (oldData ?? []).filter( (config) => config.id !== vars.configId @@ -139,8 +144,9 @@ export const useNodebalancerConfigDeleteMutation = (nodebalancerId: number) => ); export const useAllNodeBalancerConfigsQuery = (id: number) => - useQuery([queryKey, id, 'configs'], () => - getAllNodeBalancerConfigs(id) + useQuery( + [queryKey, 'nodebalanacer', id, 'configs'], + () => getAllNodeBalancerConfigs(id) ); export const getAllNodeBalancerConfigs = (id: number) => @@ -161,7 +167,7 @@ export const useAllNodeBalancersQuery = (enabled = true) => export const useInfiniteNodebalancersQuery = (filter: Filter) => useInfiniteQuery, APIError[]>( - [queryKey, filter], + [queryKey, 'infinite', filter], ({ pageParam }) => getNodeBalancers({ page: pageParam, page_size: 25 }, filter), { @@ -173,3 +179,25 @@ export const useInfiniteNodebalancersQuery = (filter: Filter) => }, } ); + +export const nodebalanacerEventHandler = (event: Event) => { + if (event.action.startsWith('nodebalancer_config')) { + queryClient.invalidateQueries([ + queryKey, + 'nodebalancer', + event.entity!.id, + 'configs', + ]); + } else { + queryClient.invalidateQueries([queryKey, 'all']); + queryClient.invalidateQueries([queryKey, 'paginated']); + queryClient.invalidateQueries([queryKey, 'infinite']); + if (event.entity?.id) { + queryClient.invalidateQueries([ + queryKey, + 'nodebalancer', + event.entity.id, + ]); + } + } +}; From 88dde09d4447c33438edef5ea76de809b5fc9b19 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 31 Mar 2023 15:22:03 -0400 Subject: [PATCH 03/18] fix error message --- .../NodeBalancersLanding/NodeBalancersLanding.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx index ea8d8a49ef9..6bf9e186d04 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx @@ -16,7 +16,6 @@ import TransferDisplay from 'src/components/TransferDisplay'; import { useOrder } from 'src/hooks/useOrder'; import usePagination from 'src/hooks/usePagination'; import { useNodeBalancersQuery } from 'src/queries/nodebalancers'; -import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; import NodeBalancersLandingEmptyState from './NodeBalancersLandingEmptyState'; import { NodeBalancerTableRow } from './NodeBalancerTableRow'; @@ -68,9 +67,7 @@ export const NodeBalancersLanding = () => { if (error) { return ( ); } From 998651e2b8e8b46d6c99c681e831be8ad92a373f Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 31 Mar 2023 15:29:37 -0400 Subject: [PATCH 04/18] hook up event handler --- packages/manager/src/App.tsx | 7 +++++++ .../src/features/NodeBalancers/NodeBalancerCreate.tsx | 2 +- .../features/NodeBalancers/NodeBalancerDeleteDialog.tsx | 1 + packages/manager/src/queries/nodebalancers.ts | 3 ++- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/App.tsx b/packages/manager/src/App.tsx index 8760e7490ce..79c561024a9 100644 --- a/packages/manager/src/App.tsx +++ b/packages/manager/src/App.tsx @@ -39,6 +39,7 @@ import { loadScript } from './hooks/useScript'; import { getNextThemeValue } from './utilities/theme'; import { sshKeyEventHandler } from './queries/profile'; import { firewallEventsHandler } from './queries/firewalls'; +import { nodebalanacerEventHandler } from './queries/nodebalancers'; interface Props { location: RouteComponentProps['location']; @@ -150,6 +151,12 @@ export class App extends React.Component { .filter((event) => event.action.startsWith('firewall') && !event._initial) .subscribe(firewallEventsHandler); + events$ + .filter( + (event) => event.action.startsWith('nodebalancer') && !event._initial + ) + .subscribe(nodebalanacerEventHandler); + /* * We want to listen for migration events side-wide * It's unpredictable when a migration is going to happen. It could take diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index 516906abc87..e20e5c2c2a0 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -318,7 +318,7 @@ class NodeBalancerCreate extends React.Component { createNodeBalancer(nodeBalancerRequestData) .then((nodeBalancer) => { - queryClient.invalidateQueries([queryKey]); + queryClient.invalidateQueries([queryKey, 'paginated']); queryClient.setQueryData( [queryKey, 'nodebalancer', nodeBalancer.id], nodeBalancer diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx index 1d5a55f3c4e..00eb758f23b 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx @@ -23,6 +23,7 @@ export const NodeBalancerDeleteDialog = ({ const onDelete = async () => { await mutateAsync(); + onClose(); push('/nodebalancers'); }; diff --git a/packages/manager/src/queries/nodebalancers.ts b/packages/manager/src/queries/nodebalancers.ts index de0a06a919b..e973dcc0882 100644 --- a/packages/manager/src/queries/nodebalancers.ts +++ b/packages/manager/src/queries/nodebalancers.ts @@ -146,7 +146,8 @@ export const useNodebalancerConfigDeleteMutation = (nodebalancerId: number) => export const useAllNodeBalancerConfigsQuery = (id: number) => useQuery( [queryKey, 'nodebalanacer', id, 'configs'], - () => getAllNodeBalancerConfigs(id) + () => getAllNodeBalancerConfigs(id), + { refetchInterval: 20 } ); export const getAllNodeBalancerConfigs = (id: number) => From 405aebb62ba26246169cc6b205c1da80800f63f4 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 31 Mar 2023 18:02:24 -0400 Subject: [PATCH 05/18] clean up --- .../src/containers/regions.container.tsx | 10 ++++---- .../LinodesCreate/SelectPlanPanel.test.tsx | 3 --- .../linodes/LinodesCreate/SelectPlanPanel.tsx | 23 +++++++------------ 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/packages/manager/src/containers/regions.container.tsx b/packages/manager/src/containers/regions.container.tsx index 50cc4227361..29ff9d8a596 100644 --- a/packages/manager/src/containers/regions.container.tsx +++ b/packages/manager/src/containers/regions.container.tsx @@ -3,11 +3,10 @@ import { APIError } from '@linode/api-v4/lib/types'; import { useRegionsQuery } from 'src/queries/regions'; import { Region } from '@linode/api-v4/lib/regions'; -export interface DefaultProps { +export interface RegionsProps { regionsData: Region[]; regionsError?: APIError[]; regionsLoading: boolean; - regionsLastUpdated: number; } /** @@ -23,16 +22,15 @@ export interface DefaultProps { * and pass regions through there. */ type Wrapper = ( - Component: React.ComponentType + Component: React.ComponentType ) => React.FC; const regionsContainer: Wrapper = ( - Component: React.ComponentType + Component: React.ComponentType ) => (props) => { - const { data, error, isLoading, dataUpdatedAt } = useRegionsQuery(); + const { data, error, isLoading } = useRegionsQuery(); return ( { currentPlanHeading="Linode 2GB" selectedID="test" onSelect={jest.fn()} - regionsData={[]} - regionsLoading={false} - regionsLastUpdated={0} /> ); getByText(/linode plan/i); diff --git a/packages/manager/src/features/linodes/LinodesCreate/SelectPlanPanel.tsx b/packages/manager/src/features/linodes/LinodesCreate/SelectPlanPanel.tsx index a70913c4a06..d460cbdde4b 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/SelectPlanPanel.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/SelectPlanPanel.tsx @@ -5,7 +5,6 @@ import classNames from 'classnames'; import { LDClient } from 'launchdarkly-js-client-sdk'; import { isEmpty, pathOr } from 'ramda'; import * as React from 'react'; -import { compose } from 'recompose'; import Chip from 'src/components/core/Chip'; import FormControlLabel from 'src/components/core/FormControlLabel'; import Hidden from 'src/components/core/Hidden'; @@ -19,7 +18,7 @@ import Grid from '@mui/material/Unstable_Grid2'; import HelpIcon from 'src/components/HelpIcon'; import Notice from 'src/components/Notice'; import Radio from 'src/components/Radio'; -import RenderGuard, { RenderGuardProps } from 'src/components/RenderGuard'; +import RenderGuard from 'src/components/RenderGuard'; import SelectionCard from 'src/components/SelectionCard'; import TabbedPanel from 'src/components/TabbedPanel'; import { Tab } from 'src/components/TabbedPanel/TabbedPanel'; @@ -27,14 +26,12 @@ import Table from 'src/components/Table'; import TableCell from 'src/components/TableCell'; import TableRow from 'src/components/TableRow'; import { LINODE_NETWORK_IN } from 'src/constants'; -import withRegions, { - DefaultProps as RegionsProps, -} from 'src/containers/regions.container'; import arrayToList from 'src/utilities/arrayToDelimiterSeparatedList'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; import { gpuPlanText } from './utilities'; import { ExtendedType } from 'src/utilities/extendType'; import { ApplicationState } from 'src/store'; +import { useRegionsQuery } from 'src/queries/regions'; const useStyles = makeStyles((theme: Theme) => ({ root: { @@ -153,9 +150,7 @@ const getMetal = (types: PlanSelectionType[]) => const getPremium = (types: PlanSelectionType[]) => types.filter((t: PlanSelectionType) => t.class === 'premium'); -type CombinedProps = Props & RegionsProps; - -export const SelectPlanPanel: React.FC = (props) => { +export const SelectPlanPanel = (props: Props) => { const { selectedID, currentPlanHeading, @@ -171,6 +166,8 @@ export const SelectPlanPanel: React.FC = (props) => { docsLink, } = props; + const { data: regions } = useRegionsQuery(); + const selectedLinodePlanType = useSelector((state: ApplicationState) => { if (linodeID) { return state?.__resources.linodes.itemsById[linodeID]?.type; @@ -188,11 +185,10 @@ export const SelectPlanPanel: React.FC = (props) => { }; const getRegionsWithCapability = (capability: Capabilities) => { - const regions = props.regionsData ?? []; const withCapability = regions - .filter((thisRegion) => thisRegion.capabilities.includes(capability)) + ?.filter((thisRegion) => thisRegion.capabilities.includes(capability)) .map((thisRegion) => thisRegion.label); - return arrayToList(withCapability); + return arrayToList(withCapability ?? []); }; const renderSelection = (type: PlanSelectionType, idx: number) => { @@ -622,7 +618,4 @@ export const SelectPlanPanel: React.FC = (props) => { ); }; -export default compose( - RenderGuard, - withRegions -)(SelectPlanPanel); +export default RenderGuard(SelectPlanPanel); From 6e2f6bcdf92c9776ad2c43aa76bdadf440e90bbd Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Fri, 31 Mar 2023 18:42:12 -0400 Subject: [PATCH 06/18] remove duplicate regions type --- .../src/features/NodeBalancers/NodeBalancerCreate.tsx | 11 ++--------- .../features/linodes/LinodesCreate/LinodeCreate.tsx | 4 ++-- .../linodes/LinodesCreate/LinodeCreateContainer.tsx | 5 ++--- .../src/features/linodes/LinodesCreate/types.ts | 6 ------ 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index e20e5c2c2a0..eed387e75c2 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -30,7 +30,7 @@ import Notice from 'src/components/Notice'; import SelectRegionPanel from 'src/components/SelectRegionPanel'; import { Tag } from 'src/components/TagsInput'; import withProfile, { ProfileProps } from 'src/components/withProfile'; -import withRegions from 'src/containers/regions.container'; +import withRegions, { RegionsProps } from 'src/containers/regions.container'; import { hasGrant } from 'src/features/Profile/permissionsHelpers'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { isEURegion } from 'src/utilities/formatRegion'; @@ -55,7 +55,6 @@ import { reportAgreementSigningError, } from 'src/queries/accountAgreements'; import LandingHeader from 'src/components/LandingHeader'; -import { Region } from '@linode/api-v4/lib/regions'; import { createNodeBalancer } from '@linode/api-v4/lib/nodebalancers'; import { queryKey } from 'src/queries/nodebalancers'; @@ -80,7 +79,7 @@ const styles = (theme: Theme) => }); type CombinedProps = ProfileProps & - WithRegions & + RegionsProps & RouteComponentProps<{}> & WithStyles & AgreementsProps; @@ -846,12 +845,6 @@ export const fieldErrorsToNodePathErrors = (errors: APIError[]) => { }, []); }; -interface WithRegions { - regionsData: Region[]; - regionsLoading: boolean; - regionsError: APIError[]; -} - export default recompose( withRegions, styled, diff --git a/packages/manager/src/features/linodes/LinodesCreate/LinodeCreate.tsx b/packages/manager/src/features/linodes/LinodesCreate/LinodeCreate.tsx index cc93f7e7f30..0434c458791 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/LinodeCreate.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/LinodeCreate.tsx @@ -66,12 +66,12 @@ import { TypeInfo, WithDisplayData, WithLinodesProps, - WithRegionsProps, WithTypesRegionsAndImages, } from './types'; import UserDataAccordion from './UserDataAccordion/UserDataAccordion'; import { extendType } from 'src/utilities/extendType'; import { WithTypesProps } from 'src/containers/types.container'; +import { RegionsProps } from 'src/containers/regions.container'; type ClassNames = | 'form' @@ -189,7 +189,7 @@ type CombinedProps = Props & WithDisplayData & ImagesProps & WithLinodesProps & - WithRegionsProps & + RegionsProps & WithStyles & WithTypesProps & RouteComponentProps<{}> & diff --git a/packages/manager/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx b/packages/manager/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx index a019ce48997..b418e072a86 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/packages/manager/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -24,7 +24,7 @@ import withProfile, { ProfileProps } from 'src/components/withProfile'; import withImages, { DefaultProps as ImagesProps, } from 'src/containers/images.container'; -import withRegions from 'src/containers/regions.container'; +import withRegions, { RegionsProps } from 'src/containers/regions.container'; import { withTypes, WithTypesProps } from 'src/containers/types.container'; import withFlags, { FeatureFlagConsumerProps, @@ -66,7 +66,6 @@ import { Info, TypeInfo, WithLinodesProps, - WithRegionsProps, } from './types'; import { getRegionIDFromLinodeID } from './utilities'; import { ExtendedType, extendType } from 'src/utilities/extendType'; @@ -113,7 +112,7 @@ type CombinedProps = WithSnackbarProps & ImagesProps & WithTypesProps & WithLinodesProps & - WithRegionsProps & + RegionsProps & DispatchProps & LabelProps & FeatureFlagConsumerProps & diff --git a/packages/manager/src/features/linodes/LinodesCreate/types.ts b/packages/manager/src/features/linodes/LinodesCreate/types.ts index 960b71901fc..a959bf05470 100644 --- a/packages/manager/src/features/linodes/LinodesCreate/types.ts +++ b/packages/manager/src/features/linodes/LinodesCreate/types.ts @@ -44,12 +44,6 @@ export interface WithLinodesProps { linodesError?: APIError[]; } -export interface WithRegionsProps { - regionsData: Region[]; - regionsLoading: boolean; - regionsError?: APIError[]; -} - /** * Pure Data without the loading and error * keys. Component with these props have already been From fa2f89f98621b28c49ea321f16fb81791ba68cf8 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 4 Apr 2023 17:13:33 -0400 Subject: [PATCH 07/18] minor improvemnts --- .../EntityTable/EntityTable.stories.mdx | 72 ------------------- .../NodeBalancerTableRow.tsx | 16 ++--- .../NodeBalancersLanding.tsx | 6 +- packages/manager/src/queries/nodebalancers.ts | 2 +- 4 files changed, 11 insertions(+), 85 deletions(-) delete mode 100644 packages/manager/src/components/EntityTable/EntityTable.stories.mdx diff --git a/packages/manager/src/components/EntityTable/EntityTable.stories.mdx b/packages/manager/src/components/EntityTable/EntityTable.stories.mdx deleted file mode 100644 index 66645648090..00000000000 --- a/packages/manager/src/components/EntityTable/EntityTable.stories.mdx +++ /dev/null @@ -1,72 +0,0 @@ -import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'; -import { partition } from 'ramda'; -import { Provider } from 'react-redux'; -import { useTheme } from '@mui/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { - domainFactory, - firewallFactory, - volumeFactory, - imageFactory, - nodeBalancerFactory, - nodeBalancerConfigFactory, -} from 'src/factories'; -import store from 'src/store'; -import { headers as firewallHeaders } from 'src/features/Firewalls/FirewallLanding/FirewallLanding'; -import { headers as nodeBalancerHeaders } from 'src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding'; -import DomainRow from 'src/features/Domains/DomainTableRow'; -import FirewallRow from 'src/features/Firewalls/FirewallLanding/FirewallRow'; -import ImageRow from 'src/features/Images/ImageRow'; -import NodeBalancerTableRow from 'src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow'; -import EntityTable from './EntityTable'; - - - -export const domainHeaders = () => { - const theme = useTheme(); - const matchesXsDown = useMediaQuery(theme.breakpoints.down('xs')); - const matchesSmDown = useMediaQuery(theme.breakpoints.down('sm')); - const matchesMdDown = useMediaQuery(theme.breakpoints.down('md')); - return getDomainHeaders(matchesXsDown, matchesSmDown, matchesMdDown); -}; - -export const getNodeBalancers = () => { - const configs = nodeBalancerConfigFactory.buildList(2); - return nodeBalancerFactory.buildList(10).map((nb) => ({ ...nb, configs })); -}; - -## Firewalls - - - - - -## NodeBalancers - - - false} - isGroupedByTag={false} - headers={nodeBalancerHeaders} - row={{ - Component: NodeBalancerTableRow, - data: getNodeBalancers(), - handlers: {}, - lastUpdated: 1637782175231, - }} - initialOrder={{ order: 'asc', orderBy: 'label' }} - /> - diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx index 1e71497958c..67cbfb1c603 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx @@ -56,7 +56,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ })); interface Props extends NodeBalancer { - onDelete: (id: number) => void; + onDelete: () => void; } export const NodeBalancerTableRow = (props: Props) => { @@ -65,14 +65,12 @@ export const NodeBalancerTableRow = (props: Props) => { const { data: configs } = useAllNodeBalancerConfigsQuery(id); - const nodesUp = configs?.reduce( - (result, config) => config.nodes_status.up + result, - 0 - ); - const nodesDown = configs?.reduce( - (result, config) => config.nodes_status.down + result, - 0 - ); + const nodesUp = + configs?.reduce((result, config) => config.nodes_status.up + result, 0) ?? + 0; + const nodesDown = + configs?.reduce((result, config) => config.nodes_status.down + result, 0) ?? + 0; return ( { orderBy: 'label', order: 'asc', }, - `${preferenceKey}-order` + preferenceKey ); const filter = { @@ -125,14 +125,14 @@ export const NodeBalancersLanding = () => { {data?.data.map((nodebalancer) => ( onDelete(nodebalancer.id)} {...nodebalancer} /> ))} useQuery( [queryKey, 'nodebalanacer', id, 'configs'], () => getAllNodeBalancerConfigs(id), - { refetchInterval: 20 } + { refetchInterval: 20000 } ); export const getAllNodeBalancerConfigs = (id: number) => From e6b351c5d33fc5315c9814eab51bb7002ca74af6 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 4 Apr 2023 17:23:18 -0400 Subject: [PATCH 08/18] fix config open linking --- .../NodeBalancerSummary/SummaryPanel.tsx | 11 +--- .../features/NodeBalancers/NodeBalancers.tsx | 2 +- .../NodeBalancerTableRow.tsx | 66 ++++--------------- 3 files changed, 16 insertions(+), 63 deletions(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index 266d581d9ed..ea9fa66b50b 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -92,23 +92,19 @@ const SummaryPanel = () => {
- + Backend Status: {`${up} up, ${down} down`}
- + Transferred: {convertMegabytesTo(nodebalancer.transfer.total)}
- + Host Name: {nodebalancer.hostname} @@ -119,7 +115,6 @@ const SummaryPanel = () => {
- IP Addresses diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx index 308fef05c2e..1abd1b38eb3 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx @@ -22,7 +22,7 @@ const NodeBalancers = () => { /> diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx index 67cbfb1c603..d0cebc8b993 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx @@ -11,20 +11,9 @@ import { useAllNodeBalancerConfigsQuery } from 'src/queries/nodebalancers'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; import { NodeBalancerActionMenu } from './NodeBalancerActionMenu'; import { Theme } from '@mui/material/styles'; +import Skeleton from 'src/components/Skeleton'; const useStyles = makeStyles()((theme: Theme) => ({ - // @todo: temporary measure that will cause scroll for the 'Name' and 'Backend Status' - // column until we implement a hideOnTablet prop for EntityTables to prevent the - // ActionCell from being misaligned - labelWrapper: { - display: 'flex', - flexFlow: 'row nowrap', - alignItems: 'center', - whiteSpace: 'nowrap', - }, - statusWrapper: { - whiteSpace: 'nowrap', - }, portLink: { color: theme.textColors.linkActiveLight, }, @@ -36,23 +25,6 @@ const useStyles = makeStyles()((theme: Theme) => ({ textDecoration: 'underline', }, }, - ipsWrapper: { - display: 'inline-flex', - flexDirection: 'column', - '& [data-qa-copy-ip] button > svg': { - opacity: 0, - }, - }, - row: { - '&:hover': { - '& [data-qa-copy-ip] button > svg': { - opacity: 1, - }, - }, - '& [data-qa-copy-ip] button:focus > svg': { - opacity: 1, - }, - }, })); interface Props extends NodeBalancer { @@ -73,33 +45,21 @@ export const NodeBalancerTableRow = (props: Props) => { 0; return ( - - -
- - {label} - -
+ + + + {label} + - + {nodesUp} up - {nodesDown} down - - {convertMegabytesTo(transfer.total)} - - + {convertMegabytesTo(transfer.total)} + + {!configs ? : null} {configs?.length === 0 && 'None'} {configs?.map(({ port, id: configId }, i) => ( @@ -114,10 +74,8 @@ export const NodeBalancerTableRow = (props: Props) => { ))} - -
- -
+ + From 4d1616ca53c0aba979c6aaac2dda031a04865649 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 4 Apr 2023 17:37:32 -0400 Subject: [PATCH 09/18] update `usePagination` import --- .../NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx index edb8afae2b7..f58316b385b 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx @@ -14,7 +14,7 @@ import TableCell from 'src/components/TableCell/TableCell'; import { TableSortCell } from 'src/components/TableSortCell/TableSortCell'; import TransferDisplay from 'src/components/TransferDisplay'; import { useOrder } from 'src/hooks/useOrder'; -import usePagination from 'src/hooks/usePagination'; +import { usePagination } from 'src/hooks/usePagination'; import { useNodeBalancersQuery } from 'src/queries/nodebalancers'; import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; import NodeBalancersLandingEmptyState from './NodeBalancersLandingEmptyState'; From d92661a580c1d476b06d21d49328a2b9d17477ed Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 4 Apr 2023 17:42:27 -0400 Subject: [PATCH 10/18] clean up crazy styles --- .../NodeBalancerTableRow.tsx | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx index d0cebc8b993..30bbbd97d1a 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; import { Link } from 'react-router-dom'; import Hidden from 'src/components/core/Hidden'; -import { makeStyles } from 'tss-react/mui'; import TableCell from 'src/components/TableCell'; import TableRow from 'src/components/TableRow'; import IPAddress from 'src/features/linodes/LinodesLanding/IPAddress'; @@ -10,29 +9,13 @@ import RegionIndicator from 'src/features/linodes/LinodesLanding/RegionIndicator import { useAllNodeBalancerConfigsQuery } from 'src/queries/nodebalancers'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; import { NodeBalancerActionMenu } from './NodeBalancerActionMenu'; -import { Theme } from '@mui/material/styles'; import Skeleton from 'src/components/Skeleton'; -const useStyles = makeStyles()((theme: Theme) => ({ - portLink: { - color: theme.textColors.linkActiveLight, - }, - link: { - display: 'block', - color: theme.textColors.linkActiveLight, - lineHeight: '1.125rem', - '&:hover, &:focus': { - textDecoration: 'underline', - }, - }, -})); - interface Props extends NodeBalancer { onDelete: () => void; } export const NodeBalancerTableRow = (props: Props) => { - const { classes } = useStyles(); const { id, label, transfer, ipv4, region, onDelete } = props; const { data: configs } = useAllNodeBalancerConfigsQuery(id); @@ -47,7 +30,7 @@ export const NodeBalancerTableRow = (props: Props) => { return ( - + {label} @@ -63,10 +46,7 @@ export const NodeBalancerTableRow = (props: Props) => { {configs?.length === 0 && 'None'} {configs?.map(({ port, id: configId }, i) => ( - + {port} {i < configs.length - 1 ? ', ' : ''} From 3e5c9972c45f8da6471bbd144ab120a8d4fa6e3c Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 4 Apr 2023 18:20:41 -0400 Subject: [PATCH 11/18] made nodebalancer create look acceptable --- .../LabelAndTagsPanel/LabelAndTagsPanel.tsx | 3 +- .../NodeBalancers/NodeBalancerCreate.tsx | 446 ++++++++---------- 2 files changed, 203 insertions(+), 246 deletions(-) diff --git a/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx b/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx index 30562ba5141..183b4e50942 100644 --- a/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx +++ b/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx @@ -15,7 +15,7 @@ const styles = (theme: Theme) => root: { flexGrow: 1, width: '100%', - marginTop: theme.spacing(3), + marginTop: theme.spacing(1), backgroundColor: theme.color.white, }, expPanelButton: { @@ -46,6 +46,7 @@ export class InfoPanel extends React.Component { placeholder: 'Enter a label', })} data-qa-label-input + noMarginTop /> {tagsInputProps && }
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index eed387e75c2..bb4ee9c3e46 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -16,7 +16,6 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'; import { compose as recompose } from 'recompose'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; -import CheckoutBar, { DisplaySectionList } from 'src/components/CheckoutBar'; import CircleProgress from 'src/components/CircleProgress'; import ConfirmationDialog from 'src/components/ConfirmationDialog'; import Paper from 'src/components/core/Paper'; @@ -24,7 +23,6 @@ import { createStyles, withStyles, WithStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import Grid from 'src/components/Grid'; import LabelAndTagsPanel from 'src/components/LabelAndTagsPanel'; import Notice from 'src/components/Notice'; import SelectRegionPanel from 'src/components/SelectRegionPanel'; @@ -57,6 +55,8 @@ import { import LandingHeader from 'src/components/LandingHeader'; import { createNodeBalancer } from '@linode/api-v4/lib/nodebalancers'; import { queryKey } from 'src/queries/nodebalancers'; +import Box from 'src/components/core/Box'; +import { CheckoutSummary } from 'src/components/CheckoutSummary/CheckoutSummary'; type ClassNames = 'title' | 'sidebar'; @@ -503,21 +503,9 @@ class NodeBalancerCreate extends React.Component { !agreements.data?.eu_model ); - const { region } = this.state.nodeBalancerFields; - let displaySections; - if (region) { - const foundRegion = regionsData.find((r) => r.id === region); - if (foundRegion) { - displaySections = [ - { - title: foundRegion.country, - details: foundRegion.label, - }, - ]; - } else { - displaySections = [{ title: 'Unknown Region' }]; - } - } + const regionLabel = this.props.regionsData.find( + (r) => r.id === nodeBalancerFields.region + )?.label; if (this.props.regionsLoading) { return ; @@ -526,236 +514,204 @@ class NodeBalancerCreate extends React.Component { return ( - - - - {generalError && !this.disabled && ( - - {generalError} - - )} - {this.disabled && ( - - )} - ({ - label: tag, - value: tag, - })) - : [], - onChange: this.tagsChange, - tagError: hasErrorFor('tags'), - disabled: this.disabled, - }} - /> - - - - NodeBalancer Settings - - - - {this.state.nodeBalancerFields.configs.map( - (nodeBalancerConfig, idx) => { - const lensTo = lensFrom([ - 'nodeBalancerFields', - 'configs', - idx, - ]); - - const L = { - algorithmLens: lensTo(['algorithm']), - checkPassiveLens: lensTo(['check_passive']), - checkBodyLens: lensTo(['check_body']), - checkPathLens: lensTo(['check_path']), - portLens: lensTo(['port']), - protocolLens: lensTo(['protocol']), - proxyProtocolLens: lensTo(['proxy_protocol']), - healthCheckTypeLens: lensTo(['check']), - healthCheckAttemptsLens: lensTo(['check_attempts']), - healthCheckIntervalLens: lensTo(['check_interval']), - healthCheckTimeoutLens: lensTo(['check_timeout']), - sessionStickinessLens: lensTo(['stickiness']), - sslCertificateLens: lensTo(['ssl_cert']), - privateKeyLens: lensTo(['ssl_key']), - }; - - return ( - - - this.onNodeLabelChange(idx, nodeIndex, value) - } - onNodeAddressChange={(nodeIndex, value) => - this.onNodeAddressChange(idx, nodeIndex, value) - } - onNodePortChange={(nodeIndex, value) => - this.onNodePortChange(idx, nodeIndex, value) - } - onNodeWeightChange={(nodeIndex, value) => - this.onNodeWeightChange(idx, nodeIndex, value) - } - onDelete={this.onDeleteConfig(idx)} - disabled={this.disabled} - /> - - ); - } - )} - - - - - - - - this.setState({ signedAgreement: e.target.checked }) - } - /> - ) : undefined + /> + + ); + } + )} + + acc + config.nodes.length, + 0 + ), + }, + ].filter((item) => Boolean(item.title))} + /> + + {showAgreement ? ( + + this.setState({ signedAgreement: e.target.checked }) } - > - - - - - + /> + ) : undefined} + + Date: Tue, 4 Apr 2023 18:23:32 -0400 Subject: [PATCH 12/18] improve validation --- packages/validation/src/nodebalancers.schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/validation/src/nodebalancers.schema.ts b/packages/validation/src/nodebalancers.schema.ts index e6b72814c5c..0f32961cde5 100644 --- a/packages/validation/src/nodebalancers.schema.ts +++ b/packages/validation/src/nodebalancers.schema.ts @@ -121,6 +121,7 @@ export const UpdateNodeBalancerConfigSchema = object({ export const NodeBalancerSchema = object({ label: string() + .required('Label is required.') .min(3, LABEL_WARNING) .max(32, LABEL_WARNING) .matches( From 19e213e13535a5a9028cd457d36bab61cfafb333 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 4 Apr 2023 21:50:31 -0400 Subject: [PATCH 13/18] more minor improvements --- .../LabelAndTagsPanel/LabelAndTagsPanel.tsx | 2 +- .../NodeBalancers/NodeBalancerConfigPanel.tsx | 5 - .../NodeBalancers/NodeBalancerCreate.tsx | 303 +++++++++--------- 3 files changed, 146 insertions(+), 164 deletions(-) diff --git a/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx b/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx index 183b4e50942..ba853aebe6c 100644 --- a/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx +++ b/packages/manager/src/components/LabelAndTagsPanel/LabelAndTagsPanel.tsx @@ -15,7 +15,7 @@ const styles = (theme: Theme) => root: { flexGrow: 1, width: '100%', - marginTop: theme.spacing(1), + marginTop: theme.spacing(3), backgroundColor: theme.color.white, }, expPanelButton: { diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx index daf365c0b14..06af1b507bb 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.tsx @@ -630,11 +630,6 @@ class NodeBalancerConfigPanel extends React.Component { ]} container > - - - Port Configuration - - - createStyles({ - title: { - marginTop: theme.spacing(3), - }, - sidebar: { - [theme.breakpoints.up('md')]: { - marginTop: '60px !important', - }, - [theme.breakpoints.down('lg')]: { - '&.MuiGrid-item': { - paddingLeft: 0, - paddingRight: 0, - }, - }, - }, - }); +import Accordion from 'src/components/Accordion'; +import Paper from 'src/components/core/Paper'; +import TextField from 'src/components/TextField'; type CombinedProps = ProfileProps & RegionsProps & RouteComponentProps<{}> & - WithStyles & AgreementsProps; interface NodeBalancerFieldsState { @@ -492,7 +470,7 @@ class NodeBalancerCreate extends React.Component { ); render() { - const { classes, regionsData, agreements, profile } = this.props; + const { regionsData, agreements, profile } = this.props; const { nodeBalancerFields, signedAgreement } = this.state; const hasErrorFor = getAPIErrorFor(errorResources, this.state.errors); const generalError = hasErrorFor('none'); @@ -538,27 +516,29 @@ class NodeBalancerCreate extends React.Component { important /> )} - ({ - label: tag, - value: tag, - })) - : [], - onChange: this.tagsChange, - tagError: hasErrorFor('tags'), - disabled: this.disabled, - }} - /> + + + ({ + label: tag, + value: tag, + })) + : [] + } + onChange={this.tagsChange} + tagError={hasErrorFor('tags')} + disabled={this.disabled} + /> + { handleSelection={this.regionChange} disabled={this.disabled} /> - - NodeBalancer Settings - - {this.state.nodeBalancerFields.configs.map( - (nodeBalancerConfig, idx) => { - const lensTo = lensFrom(['nodeBalancerFields', 'configs', idx]); - - const L = { - algorithmLens: lensTo(['algorithm']), - checkPassiveLens: lensTo(['check_passive']), - checkBodyLens: lensTo(['check_body']), - checkPathLens: lensTo(['check_path']), - portLens: lensTo(['port']), - protocolLens: lensTo(['protocol']), - proxyProtocolLens: lensTo(['proxy_protocol']), - healthCheckTypeLens: lensTo(['check']), - healthCheckAttemptsLens: lensTo(['check_attempts']), - healthCheckIntervalLens: lensTo(['check_interval']), - healthCheckTimeoutLens: lensTo(['check_timeout']), - sessionStickinessLens: lensTo(['stickiness']), - sslCertificateLens: lensTo(['ssl_cert']), - privateKeyLens: lensTo(['ssl_key']), - }; - - return ( - - - this.onNodeLabelChange(idx, nodeIndex, value) - } - onNodeAddressChange={(nodeIndex, value) => - this.onNodeAddressChange(idx, nodeIndex, value) - } - onNodePortChange={(nodeIndex, value) => - this.onNodePortChange(idx, nodeIndex, value) - } - onNodeWeightChange={(nodeIndex, value) => - this.onNodeWeightChange(idx, nodeIndex, value) - } - onDelete={this.onDeleteConfig(idx)} - disabled={this.disabled} - /> - - ); - } - )} + + {this.state.nodeBalancerFields.configs.map( + (nodeBalancerConfig, idx) => { + const lensTo = lensFrom(['nodeBalancerFields', 'configs', idx]); + + const L = { + algorithmLens: lensTo(['algorithm']), + checkPassiveLens: lensTo(['check_passive']), + checkBodyLens: lensTo(['check_body']), + checkPathLens: lensTo(['check_path']), + portLens: lensTo(['port']), + protocolLens: lensTo(['protocol']), + proxyProtocolLens: lensTo(['proxy_protocol']), + healthCheckTypeLens: lensTo(['check']), + healthCheckAttemptsLens: lensTo(['check_attempts']), + healthCheckIntervalLens: lensTo(['check_interval']), + healthCheckTimeoutLens: lensTo(['check_timeout']), + sessionStickinessLens: lensTo(['stickiness']), + sslCertificateLens: lensTo(['ssl_cert']), + privateKeyLens: lensTo(['ssl_key']), + }; + + return ( + + + this.onNodeLabelChange(idx, nodeIndex, value) + } + onNodeAddressChange={(nodeIndex, value) => + this.onNodeAddressChange(idx, nodeIndex, value) + } + onNodePortChange={(nodeIndex, value) => + this.onNodePortChange(idx, nodeIndex, value) + } + onNodeWeightChange={(nodeIndex, value) => + this.onNodeWeightChange(idx, nodeIndex, value) + } + onDelete={this.onDeleteConfig(idx)} + disabled={this.disabled} + /> + + ); + } + )} + - - - ); - - render() { - const { regionsData, agreements, profile } = this.props; - const { nodeBalancerFields, signedAgreement } = this.state; - const hasErrorFor = getAPIErrorFor(errorResources, this.state.errors); - const generalError = hasErrorFor('none'); - - const showAgreement = Boolean( - isEURegion(nodeBalancerFields.region) && - !profile.data?.restricted && - !agreements.data?.eu_model - ); - - const regionLabel = this.props.regionsData.find( - (r) => r.id === nodeBalancerFields.region - )?.label; - - if (this.props.regionsLoading) { - return ; - } - - return ( - - - - {generalError && !this.disabled && ( - - {generalError} - - )} - {this.disabled && ( - - )} - - - ({ - label: tag, - value: tag, - })) - : [] - } - onChange={this.tagsChange} - tagError={hasErrorFor('tags')} - disabled={this.disabled} + {showAgreement ? ( + updateAgreements({ eu_model: e.target.checked })} /> - - - - {this.state.nodeBalancerFields.configs.map( - (nodeBalancerConfig, idx) => { - const lensTo = lensFrom(['nodeBalancerFields', 'configs', idx]); - - const L = { - algorithmLens: lensTo(['algorithm']), - checkPassiveLens: lensTo(['check_passive']), - checkBodyLens: lensTo(['check_body']), - checkPathLens: lensTo(['check_path']), - portLens: lensTo(['port']), - protocolLens: lensTo(['protocol']), - proxyProtocolLens: lensTo(['proxy_protocol']), - healthCheckTypeLens: lensTo(['check']), - healthCheckAttemptsLens: lensTo(['check_attempts']), - healthCheckIntervalLens: lensTo(['check_interval']), - healthCheckTimeoutLens: lensTo(['check_timeout']), - sessionStickinessLens: lensTo(['stickiness']), - sslCertificateLens: lensTo(['ssl_cert']), - privateKeyLens: lensTo(['ssl_key']), - }; - - return ( - - - this.onNodeLabelChange(idx, nodeIndex, value) - } - onNodeAddressChange={(nodeIndex, value) => - this.onNodeAddressChange(idx, nodeIndex, value) - } - onNodePortChange={(nodeIndex, value) => - this.onNodePortChange(idx, nodeIndex, value) - } - onNodeWeightChange={(nodeIndex, value) => - this.onNodeWeightChange(idx, nodeIndex, value) - } - onDelete={this.onDeleteConfig(idx)} - disabled={this.disabled} - /> - - ); - } - )} - - - acc + config.nodes.length, - 0 - ), - }, - ].filter((item) => Boolean(item.title))} - /> - - {showAgreement ? ( - - this.setState({ signedAgreement: e.target.checked }) - } - /> - ) : undefined} - - - - - Are you sure you want to delete this NodeBalancer Configuration? - - - - ); - } -} + + + + + + } + open={deleteConfigConfirmDialog.open} + > + + Are you sure you want to delete this NodeBalancer Configuration? + + + + ); +}; /* @todo: move to own file */ export const lensFrom = (p1: (string | number)[]) => ( @@ -789,9 +697,4 @@ export const fieldErrorsToNodePathErrors = (errors: APIError[]) => { }, []); }; -export default recompose( - withRegions, - withRouter, - withProfile, - withAgreements -)(NodeBalancerCreate); +export default NodeBalancerCreate; From 2989a587aabba37c06d7874336951ada439ea949 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 5 Apr 2023 00:44:43 -0400 Subject: [PATCH 15/18] get rid of as much `ramda` as possible --- .../NodeBalancers/NodeBalancerCreate.tsx | 287 ++++++++---------- 1 file changed, 123 insertions(+), 164 deletions(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index 7d429100ea0..d7ea0d48326 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -4,12 +4,9 @@ import { clone, compose, defaultTo, - Lens, lensPath, over, pathOr, - set, - view, } from 'ramda'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; @@ -119,21 +116,25 @@ const NodeBalancerCreate = () => { }; const addNodeBalancerConfigNode = (configIdx: number) => () => - setNodeBalancerFields( - over( - lensPath(['configs', configIdx, 'nodes']), - append(createNewNodeBalancerConfigNode()) - ) - ); + setNodeBalancerFields((prev) => { + const newConfigs = [...prev.configs]; + newConfigs[configIdx].nodes = [ + ...newConfigs[configIdx].nodes, + createNewNodeBalancerConfigNode(), + ]; + return { ...prev, configs: newConfigs }; + }); const removeNodeBalancerConfigNode = (configIdx: number) => ( nodeIdx: number ) => - setNodeBalancerFields( - over(lensPath(['configs', configIdx, 'nodes']), (nodes) => - nodes.filter((n: any, idx: number) => idx !== nodeIdx) - ) - ); + setNodeBalancerFields((prev) => { + const newConfigs = [...prev.configs]; + newConfigs[configIdx].nodes = newConfigs[configIdx].nodes.filter( + (_, idx) => idx !== nodeIdx + ); + return { ...prev, configs: newConfigs }; + }); const setNodeValue = ( cidx: number, @@ -141,9 +142,11 @@ const NodeBalancerCreate = () => { key: string, value: any ) => - setNodeBalancerFields( - set(lensPath(['configs', cidx, 'nodes', nodeidx, key]), value) - ); + setNodeBalancerFields((prev) => { + const newConfigs = [...prev.configs]; + newConfigs[cidx].nodes[nodeidx][key] = value; + return { ...prev, configs: newConfigs }; + }); const onNodeLabelChange = ( configIdx: number, @@ -171,63 +174,47 @@ const NodeBalancerCreate = () => { value: string ) => setNodeValue(configIdx, nodeIdx, 'weight', value); - const afterProtocolUpdate = (L: { [key: string]: Lens }) => () => { - setNodeBalancerFields( - // @ts-expect-error compose is trash - compose(set(L.sslCertificateLens, ''), set(L.privateKeyLens, '')) - ); + const afterProtocolUpdate = (configIdx: number) => { + setNodeBalancerFields((prev) => { + const newConfigs = [...prev.configs]; + newConfigs[configIdx].ssl_cert = ''; + newConfigs[configIdx].ssl_key = ''; + return { ...prev, configs: newConfigs }; + }); }; - const afterHealthCheckTypeUpdate = (L: { [key: string]: Lens }) => () => { - setNodeBalancerFields( - // @ts-expect-error compose is trash - compose( - set(L.checkPathLens, defaultFieldsStates.configs[0].check_path), - set(L.checkBodyLens, defaultFieldsStates.configs[0].check_body), - set( - L.healthCheckAttemptsLens, - defaultFieldsStates.configs[0].check_attempts - ) as () => any, - set( - L.healthCheckIntervalLens, - defaultFieldsStates.configs[0].check_interval - ), - set( - L.healthCheckTimeoutLens, - defaultFieldsStates.configs[0].check_timeout - ) as () => any - ) - ); - }; + const afterHealthCheckTypeUpdate = (configIdx: number) => { + setNodeBalancerFields((prev) => { + const newConfigs = [...prev.configs]; + newConfigs[configIdx].check_path = + defaultFieldsStates.configs[0].check_path; - const clearNodeErrors = () => { - // Build paths for all config errors. - const configPaths = nodeBalancerFields.configs.map((config, idxC) => { - return ['configs', idxC, 'errors']; - }); + newConfigs[configIdx].check_body = + defaultFieldsStates.configs[0].check_body; - // Build paths to all node errors - const nodePaths = nodeBalancerFields.configs.map((config, idxC) => { - return config.nodes.map((nodes, idxN) => { - return ['configs', idxC, 'nodes', idxN, 'errors']; - }); - }); + newConfigs[configIdx].check_attempts = + defaultFieldsStates.configs[0].check_attempts; - const paths = [ - ...configPaths, - ...nodePaths.reduce((acc, pathArr) => [...acc, ...pathArr], []), - ]; + newConfigs[configIdx].check_timeout = + defaultFieldsStates.configs[0].check_timeout; - if (paths.length === 0) { - return; - } + newConfigs[configIdx].check_interval = + defaultFieldsStates.configs[0].check_interval; - /* Map those paths to an array of updater functions */ - const setFns = paths.map((path: any[]) => { - return set(lensPath([...path]), []); + return { ...prev, configs: newConfigs }; + }); + }; + + const clearNodeErrors = () => { + setNodeBalancerFields((prev) => { + const newConfigs = [...prev.configs].map((config) => ({ + ...config, + errors: [], + nodes: config.nodes.map((node) => ({ ...node, errors: [] })), + })); + + return { ...prev, configs: newConfigs }; }); - /* Apply all of those update functions at once to state */ - setNodeBalancerFields((compose as any)(...setFns)); }; const setNodeErrors = (errors: APIError[]) => { @@ -316,17 +303,26 @@ const NodeBalancerCreate = () => { setDeleteConfigConfirmDialog(clone(defaultDeleteConfigConfirmDialogState)); }; + const onConfigValueChange = (configId: number, key: string, value: any) => { + setNodeBalancerFields((prev) => { + const newConfigs = [...prev.configs]; + newConfigs[configId][key] = value; + return { ...prev, configs: newConfigs }; + }); + }; + const labelChange = (e: React.ChangeEvent) => { - setNodeBalancerFields(set(lensPath(['label']), e.target.value)); + setNodeBalancerFields((prev) => ({ + ...prev, + label: e.target.value, + })); }; const tagsChange = (tags: Tag[]) => { - setNodeBalancerFields( - set( - lensPath(['tags']), - tags.map((tag) => tag.value) - ) - ); + setNodeBalancerFields((prev) => ({ + ...prev, + tags: tags.map((tag) => tag.value), + })); }; const resetNodeAddresses = () => { @@ -345,7 +341,10 @@ const NodeBalancerCreate = () => { }, ]; }, []); - setNodeBalancerFields(set(lensPath(['configs']), newConfigs)); + setNodeBalancerFields((prev) => ({ + ...prev, + configs: newConfigs, + })); }; const regionChange = (region: string) => { @@ -353,7 +352,12 @@ const NodeBalancerCreate = () => { if (nodeBalancerFields.region === region) { return; } - setNodeBalancerFields(set(lensPath(['region']), region)); + + setNodeBalancerFields((prev) => ({ + ...prev, + region, + })); + // We just changed the region so any selected IP addresses are likely invalid resetNodeAddresses(); }; @@ -361,18 +365,6 @@ const NodeBalancerCreate = () => { const onCloseConfirmation = () => setDeleteConfigConfirmDialog(clone(defaultDeleteConfigConfirmDialogState)); - const updateState = ( - lens: Lens, - L?: { [key: string]: Lens }, - callback?: (L: { [key: string]: Lens }) => () => void - ) => (value: any) => { - setNodeBalancerFields(set(lens, value)); - - if (L && callback) { - callback(L); - } - }; - const confirmationConfigError = () => (deleteConfigConfirmDialog.errors || []).map((e) => e.reason).join(','); @@ -447,29 +439,14 @@ const NodeBalancerCreate = () => { /> {nodeBalancerFields.configs.map((nodeBalancerConfig, idx) => { - const lensTo = lensFrom(['configs', idx]); - - const L = { - algorithmLens: lensTo(['algorithm']), - checkPassiveLens: lensTo(['check_passive']), - checkBodyLens: lensTo(['check_body']), - checkPathLens: lensTo(['check_path']), - portLens: lensTo(['port']), - protocolLens: lensTo(['protocol']), - proxyProtocolLens: lensTo(['proxy_protocol']), - healthCheckTypeLens: lensTo(['check']), - healthCheckAttemptsLens: lensTo(['check_attempts']), - healthCheckIntervalLens: lensTo(['check_interval']), - healthCheckTimeoutLens: lensTo(['check_timeout']), - sessionStickinessLens: lensTo(['stickiness']), - sslCertificateLens: lensTo(['ssl_cert']), - privateKeyLens: lensTo(['ssl_key']), - }; + const onChange = (key: keyof NodeBalancerConfigFieldsWithStatus) => ( + value: any + ) => onConfigValueChange(idx, key, value); return ( { nodeBalancerRegion={nodeBalancerFields.region} errors={nodeBalancerConfig.errors} configIdx={idx} - algorithm={view(L.algorithmLens, nodeBalancerFields)} - onAlgorithmChange={updateState(L.algorithmLens)} - checkPassive={view(L.checkPassiveLens, nodeBalancerFields)} - onCheckPassiveChange={updateState(L.checkPassiveLens)} - checkBody={view(L.checkBodyLens, nodeBalancerFields)} - onCheckBodyChange={updateState(L.checkBodyLens)} - checkPath={view(L.checkPathLens, nodeBalancerFields)} - onCheckPathChange={updateState(L.checkPathLens)} - port={view(L.portLens, nodeBalancerFields)} - onPortChange={updateState(L.portLens)} - protocol={view(L.protocolLens, nodeBalancerFields)} - proxyProtocol={view(L.proxyProtocolLens, nodeBalancerFields)} - onProtocolChange={updateState( - L.protocolLens, - L, - afterProtocolUpdate - )} - onProxyProtocolChange={updateState(L.proxyProtocolLens)} - healthCheckType={view( - L.healthCheckTypeLens, - nodeBalancerFields - )} - onHealthCheckTypeChange={updateState( - L.healthCheckTypeLens, - L, - afterHealthCheckTypeUpdate - )} - healthCheckAttempts={view( - L.healthCheckAttemptsLens, - nodeBalancerFields - )} - onHealthCheckAttemptsChange={updateState( - L.healthCheckAttemptsLens - )} - healthCheckInterval={view( - L.healthCheckIntervalLens, - nodeBalancerFields - )} - onHealthCheckIntervalChange={updateState( - L.healthCheckIntervalLens - )} - healthCheckTimeout={view( - L.healthCheckTimeoutLens, - nodeBalancerFields - )} - onHealthCheckTimeoutChange={updateState( - L.healthCheckTimeoutLens - )} - sessionStickiness={view( - L.sessionStickinessLens, - nodeBalancerFields - )} - onSessionStickinessChange={updateState(L.sessionStickinessLens)} - sslCertificate={view(L.sslCertificateLens, nodeBalancerFields)} - onSslCertificateChange={updateState(L.sslCertificateLens)} - privateKey={view(L.privateKeyLens, nodeBalancerFields)} - onPrivateKeyChange={updateState(L.privateKeyLens)} + algorithm={nodeBalancerFields.configs[idx].algorithm!} + onAlgorithmChange={onChange('algorithm')} + checkPassive={nodeBalancerFields.configs[idx].check_passive!} + onCheckPassiveChange={onChange('check_passive')} + checkBody={nodeBalancerFields.configs[idx].check_body!} + onCheckBodyChange={onChange('check_body')} + checkPath={nodeBalancerFields.configs[idx].check_path!} + onCheckPathChange={onChange('check_path')} + port={nodeBalancerFields.configs[idx].port!} + onPortChange={onChange('port')} + protocol={nodeBalancerFields.configs[idx].protocol!} + proxyProtocol={nodeBalancerFields.configs[idx].proxy_protocol!} + onProtocolChange={(value) => { + onChange('protocol')(value); + afterProtocolUpdate(idx); + }} + onProxyProtocolChange={onChange('proxy_protocol')} + healthCheckType={nodeBalancerFields.configs[idx].check!} + onHealthCheckTypeChange={(value) => { + onChange('check')(value); + afterHealthCheckTypeUpdate(idx); + }} + healthCheckAttempts={ + nodeBalancerFields.configs[idx].check_attempts! + } + onHealthCheckAttemptsChange={onChange('check_attempts')} + healthCheckInterval={ + nodeBalancerFields.configs[idx].check_interval! + } + onHealthCheckIntervalChange={onChange('check_interval')} + healthCheckTimeout={ + nodeBalancerFields.configs[idx].check_timeout! + } + onHealthCheckTimeoutChange={onChange('check_timeout')} + sessionStickiness={nodeBalancerFields.configs[idx].stickiness!} + onSessionStickinessChange={onChange('stickiness')} + sslCertificate={nodeBalancerFields.configs[idx].ssl_cert!} + onSslCertificateChange={onChange('ssl_cert')} + privateKey={nodeBalancerFields.configs[idx].ssl_key!} + onPrivateKeyChange={onChange('ssl_key')} nodes={nodeBalancerFields.configs[idx].nodes} addNode={addNodeBalancerConfigNode(idx)} removeNode={removeNodeBalancerConfigNode(idx)} @@ -560,7 +520,6 @@ const NodeBalancerCreate = () => {