Skip to content

Commit

Permalink
Hydrate redux react flow (#240)
Browse files Browse the repository at this point in the history
* chore: create react flow redux state to house nodes and edges hydrated by fetch of clusters.

* chore:move react flow helper funcs to utils

* chore: hydrate nodes and edges for react flow redux state. use implicit type attached to return of getClusters instead of explicit.

* chore: use react flow state for nodes and edges in GraphView comp
  • Loading branch information
D-B-Hawk authored Aug 21, 2023
1 parent 7ab98a8 commit 912b083
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 210 deletions.
171 changes: 12 additions & 159 deletions components/flow/index.tsx
Original file line number Diff line number Diff line change
@@ -1,155 +1,12 @@
import React, { useCallback, type FunctionComponent, MouseEvent } from 'react';
import ReactFlow, {
addEdge,
Edge,
Connection,
NodeTypes,
useNodesState,
useEdgesState,
Node,
ReactFlowProvider,
useReactFlow,
} from 'reactflow';
import ReactFlow, { NodeTypes, Node, ReactFlowProvider, useReactFlow } from 'reactflow';

import { GraphNode, type CustomGraphNode } from '../graphNode';
import 'reactflow/dist/style.css';
import { ClusterStatus, ClusterType } from '../../types/provision';
import { InstallationType } from '../../types/redux';
import { GraphNode } from '../graphNode';
import { ClusterInfo } from '../../components/clusterTable/clusterTable';
import { useAppDispatch, useAppSelector } from '../../redux/store';
import { onConnect, onEdgesChange, onNodesChange } from '../../redux/slices/reactFlow.slice';

const clusters: ClusterInfo[] = [
{
clusterName: 'kuberfirst-mgmt',
type: ClusterType.MANAGEMENT,
cloudProvider: InstallationType.AWS,
cloudRegion: 'ap-southeast-1',
creationDate: '05 Apr 2023, 12:24:56',
gitUser: 'Eleanor Carroll',
status: ClusterStatus.PROVISIONED,
adminEmail: '[email protected]',
gitProvider: 'Github',
domainName: 'yourdomain.com',
nodes: 2,
},
{
clusterName: 'kuberfirst-worker-1',
type: ClusterType.WORKLOAD,
cloudProvider: InstallationType.CIVO,
cloudRegion: 'ap-southeast-1',
nodes: 2,
creationDate: '05 Apr 2023, 12:24:56',
gitUser: 'Eleanor Carroll',
status: ClusterStatus.ERROR,
adminEmail: '[email protected]',
gitProvider: 'Github',
domainName: 'yourdomain.com',
},
{
clusterName: 'kuberfirst-worker-2',
type: ClusterType.WORKLOAD,
cloudProvider: InstallationType.DIGITAL_OCEAN,
cloudRegion: 'ap-southeast-1',
nodes: 2,
creationDate: '05 Apr 2023, 12:24:56',
gitUser: 'Eleanor Carroll',
status: ClusterStatus.DELETING,
adminEmail: '[email protected]',
gitProvider: 'Github',
domainName: 'yourdomain.com',
},
{
clusterName: 'kuberfirst-worker-3',
type: ClusterType.WORKLOAD,
cloudProvider: InstallationType.DIGITAL_OCEAN,
cloudRegion: 'ap-southeast-1',
nodes: 2,
creationDate: '05 Apr 2023, 12:24:56',
gitUser: 'Eleanor Carroll',
status: ClusterStatus.PROVISIONED,
adminEmail: '[email protected]',
gitProvider: 'Github',
domainName: 'yourdomain.com',
},
{
clusterName: 'kuberfirst-worker-4',
type: ClusterType.WORKLOAD,
cloudProvider: InstallationType.VULTR,
cloudRegion: 'ap-southeast-1',
nodes: 2,
creationDate: '05 Apr 2023, 12:24:56',
gitUser: 'Eleanor Carroll',
status: ClusterStatus.PROVISIONED,
adminEmail: '[email protected]',
gitProvider: 'Github',
domainName: 'yourdomain.com',
},
{
clusterName: 'kuberfirst-worker-5',
type: ClusterType.WORKLOAD,
cloudProvider: InstallationType.VULTR,
cloudRegion: 'ap-southeast-1',
nodes: 2,
creationDate: '05 Apr 2023, 12:24:56',
gitUser: 'Eleanor Carroll',
status: ClusterStatus.PROVISIONED,
adminEmail: '[email protected]',
gitProvider: 'Github',
domainName: 'yourdomain.com',
},
];

function generateNode(
cluster: ClusterInfo,
id: string,
position: { x: number; y: number },
): CustomGraphNode {
return {
id,
type: 'custom',
data: cluster,
position,
draggable: false,
};
}

function generateEdge(id: string, source: string, target: string): Edge {
return {
id,
source,
target,
animated: false,
type: 'straight',
style: { strokeWidth: 2, stroke: '#CBD5E1' },
};
}

function generateNodesConfig(
managementCluster: ClusterInfo,
workloadClusters: ClusterInfo[],
): [CustomGraphNode[], Edge[]] {
const managementNodeId = '1';
const nodes: CustomGraphNode[] = [
generateNode(managementCluster, managementNodeId, {
x: 0,
y: (workloadClusters.length * 200) / 2,
}),
];

const edges: Edge[] = [];

for (let i = 0; i < workloadClusters.length; i += 1) {
const generatedId = `${i + 2}`;

nodes.push(generateNode(workloadClusters[i], generatedId, { x: 600, y: i * 200 }));
edges.push(generateEdge(`edge-${i + 1}`, managementNodeId, generatedId));
}

return [nodes, edges];
}

const [managemenCluster, ...workloadClusters] = clusters;

const [initialNodes, initialEdges] = generateNodesConfig(managemenCluster, workloadClusters);
import 'reactflow/dist/style.css';

const nodeTypes: NodeTypes = {
custom: GraphNode,
Expand All @@ -160,15 +17,11 @@ interface GraphViewProps {
}

const GraphView: FunctionComponent<GraphViewProps> = ({ onNodeClick }) => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const { nodes, edges } = useAppSelector(({ reactFlow }) => reactFlow);

const { setCenter } = useReactFlow();
const dispatch = useAppDispatch();

const onConnect = useCallback(
(params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)),
[setEdges],
);
const { setCenter } = useReactFlow();

const handleNodeClick = useCallback(
(e: MouseEvent, node: Node) => {
Expand All @@ -187,12 +40,12 @@ const GraphView: FunctionComponent<GraphViewProps> = ({ onNodeClick }) => {
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodesChange={(changes) => dispatch(onNodesChange(changes))}
onEdgesChange={(changes) => dispatch(onEdgesChange(changes))}
onConnect={(connection) => dispatch(onConnect(connection))}
onNodeClick={handleNodeClick}
onConnect={onConnect}
fitView
nodeTypes={nodeTypes}
fitView
/>
);
};
Expand Down
12 changes: 4 additions & 8 deletions redux/slices/api.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
getCluster,
getClusters,
} from '../thunks/api.thunk';
import { sortClustersByType } from '../../utils/sortClusterByType';
import {
Cluster,
ClusterCreationStep,
Expand Down Expand Up @@ -115,7 +114,7 @@ const apiSlice = createSlice({
.addCase(getCluster.fulfilled, (state, { payload }: PayloadAction<Cluster>) => {
state.selectedCluster = payload;
state.loading = false;
state.status = payload.status as ClusterStatus;
state.status = payload.status;

if (state.status === ClusterStatus.DELETED) {
state.isDeleted = true;
Expand All @@ -130,14 +129,11 @@ const apiSlice = createSlice({
state.lastErrorCondition = payload.lastErrorCondition;
}
})
.addCase(getClusters.fulfilled, (state, { payload }: PayloadAction<Array<Cluster>>) => {
.addCase(getClusters.fulfilled, (state, { payload }) => {
state.loading = false;
state.isError = false;

const { managementCluster, workloadClusters } = sortClustersByType(payload);

state.managementCluster = managementCluster;
state.workloadClusters = workloadClusters;
state.managementCluster = payload.managementCluster;
state.workloadClusters = payload.workloadClusters;
})
.addCase(getClusters.rejected, (state) => {
state.loading = false;
Expand Down
1 change: 1 addition & 0 deletions redux/slices/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { installationReducer } from './installation.slice';
export { apiReducer } from './api.slice';
export { featureFlagsReducer } from './featureFlags.slice';
export { clusterReducer } from './cluster.slice';
export { reactFlowReducer } from './reactFlow.slice';
48 changes: 48 additions & 0 deletions redux/slices/reactFlow.slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import {
addEdge,
applyEdgeChanges,
applyNodeChanges,
Connection,
Edge,
EdgeChange,
Node,
NodeChange,
} from 'reactflow';

export interface ReactFlowState {
nodes: Node[];
edges: Edge[];
}

export const initialState: ReactFlowState = {
nodes: [],
edges: [],
};

const reactFlowSlice = createSlice({
name: 'react-flow',
initialState,
reducers: {
setNodes: (state, { payload }: PayloadAction<Node[]>) => {
state.nodes = payload;
},
setEdges: (state, { payload }: PayloadAction<Edge[]>) => {
state.edges = payload;
},
onNodesChange: (state, { payload }: PayloadAction<NodeChange[]>) => {
state.nodes = applyNodeChanges(payload, state.nodes);
},
onEdgesChange: (state, { payload }: PayloadAction<EdgeChange[]>) => {
state.edges = applyEdgeChanges(payload, state.edges);
},
onConnect: (state, { payload }: PayloadAction<Connection>) => {
state.edges = addEdge(payload, state.edges);
},
},
});

export const { setNodes, setEdges, onNodesChange, onEdgesChange, onConnect } =
reactFlowSlice.actions;

export const reactFlowReducer = reactFlowSlice.reducer;
2 changes: 2 additions & 0 deletions redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
featureFlagsReducer,
gitReducer,
installationReducer,
reactFlowReducer,
readinessReducer,
} from './slices';

Expand All @@ -34,6 +35,7 @@ const rootReducer = combineReducers({
api: apiReducer,
featureFlags: featureFlagsReducer,
cluster: clusterReducer,
reactFlow: reactFlowReducer,
});

const config = getPersistConfig({
Expand Down
64 changes: 21 additions & 43 deletions redux/thunks/api.thunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,10 @@ import {
import { GitOpsCatalogApp, GitOpsCatalogProps } from '../../types/gitOpsCatalog';
import { InstallValues, InstallationType } from '../../types/redux';
import { TelemetryClickEvent } from '../../types/telemetry';

const mapClusterFromRaw = (cluster: ClusterResponse): Cluster => ({
id: cluster._id,
clusterName: cluster.cluster_name,
adminEmail: cluster.alerts_email,
cloudProvider: cluster.cloud_provider,
cloudRegion: cluster.cloud_region,
domainName: cluster.domain_name,
gitAuth: cluster.gitAuth,
gitProvider: cluster.git_provider,
gitUser: cluster.git_user,
type: cluster.cluster_type,
creationDate: cluster.creation_timestamp,
lastErrorCondition: cluster.last_condition,
status: cluster.status,
vaultAuth: {
kbotPassword: cluster.vault_auth?.kbot_password,
},
checks: {
install_tools_check: cluster.install_tools_check,
domain_liveness_check: cluster.domain_liveness_check,
state_store_creds_check: cluster.state_store_creds_check,
state_store_create_check: cluster.state_store_create_check,
git_init_check: cluster.git_init_check,
kbot_setup_check: cluster.kbot_setup_check,
gitops_ready_check: cluster.gitops_ready_check,
git_terraform_apply_check: cluster.git_terraform_apply_check,
gitops_pushed_check: cluster.gitops_pushed_check,
cloud_terraform_apply_check: cluster.cloud_terraform_apply_check,
cluster_secrets_created_check: cluster.cluster_secrets_created_check,
argocd_install_check: cluster.argocd_install_check,
argocd_initialize_check: cluster.argocd_initialize_check,
argocd_create_registry_check: cluster.argocd_create_registry_check,
argocd_delete_registry_check: cluster.argocd_delete_registry_check,
vault_initialized_check: cluster.vault_initialized_check,
vault_terraform_apply_check: cluster.vault_terraform_apply_check,
users_terraform_apply_check: cluster.users_terraform_apply_check,
},
});
import { sortClustersByType } from '../../utils/sortClusterByType';
import { generateNodesConfig } from '../../utils/reactFlow';
import { mapClusterFromRaw } from '../../utils/mapClustersFromRaw';
import { setEdges, setNodes } from '../../redux/slices/reactFlow.slice';

export const createCluster = createAsyncThunk<
Cluster,
Expand Down Expand Up @@ -152,20 +117,33 @@ export const getCluster = createAsyncThunk<
});

export const getClusters = createAsyncThunk<
Array<Cluster>,
ReturnType<typeof sortClustersByType>,
void,
{
dispatch: AppDispatch;
state: RootState;
}
>('api/cluster/getClusters', async () => {
const res = await axios.get(`/api/proxy?${createQueryString('url', `/cluster`)}`);
>('api/cluster/getClusters', async (_, { dispatch }) => {
const res = await axios.get<ClusterResponse[]>(
`/api/proxy?${createQueryString('url', `/cluster`)}`,
);

if ('error' in res) {
throw res.error;
}

return (res.data && res.data.map(mapClusterFromRaw)) || [];
// make response ingestable
const mappedClusters = res.data.map(mapClusterFromRaw);
// sort management from workload clusters
const { managementCluster, workloadClusters } = sortClustersByType(mappedClusters);
// create node/edge config
if (managementCluster) {
const [nodes, edges] = generateNodesConfig(managementCluster, workloadClusters);
dispatch(setNodes(nodes));
dispatch(setEdges(edges));
}

return { managementCluster, workloadClusters };
});

export const deleteCluster = createAsyncThunk<
Expand Down
Loading

0 comments on commit 912b083

Please sign in to comment.