summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-07-20 14:09:39 +0200
committerFabian Mastenbroek <mail.fabianm@gmail.com>2021-07-20 14:09:39 +0200
commit6e3ad713111f35fc58bd2b7f1be5aeeb57eb94a8 (patch)
treebcd0466cae9792be5d594ca821d54e843b006423
parent51c759e74b088d405b63fdb3e374822308d21366 (diff)
refactor(ui): Perform Saga mutations through React Query
This change updates the OpenDC frontend to perform mutations of the topology done in Sagas through the React Query cache, so that non-Saga parts of the application also have their topology queries updated.
-rw-r--r--opendc-web/opendc-web-ui/src/data/query.js57
-rw-r--r--opendc-web/opendc-web-ui/src/data/topology.js2
-rw-r--r--opendc-web/opendc-web-ui/src/pages/_app.js23
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/objects.js36
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/query.js41
-rw-r--r--opendc-web/opendc-web-ui/src/redux/sagas/topology.js48
6 files changed, 147 insertions, 60 deletions
diff --git a/opendc-web/opendc-web-ui/src/data/query.js b/opendc-web/opendc-web-ui/src/data/query.js
new file mode 100644
index 00000000..59eaa684
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/data/query.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { useMemo } from 'react'
+import { QueryClient } from 'react-query'
+import { useAuth } from '../auth'
+import { configureExperimentClient } from './experiments'
+import { configureProjectClient } from './project'
+import { configureTopologyClient } from './topology'
+
+let queryClient
+
+function createQueryClient(auth) {
+ const client = new QueryClient()
+ configureProjectClient(client, auth)
+ configureExperimentClient(client, auth)
+ configureTopologyClient(client, auth)
+ return client
+}
+
+function initializeQueryClient(auth) {
+ const _queryClient = queryClient ?? createQueryClient(auth)
+
+ // For SSG and SSR always create a new query client
+ if (typeof window === 'undefined') return _queryClient
+ // Create the query client once in the client
+ if (!queryClient) queryClient = _queryClient
+
+ return _queryClient
+}
+
+/**
+ * Obtain a cached query client.
+ */
+export function useNewQueryClient() {
+ const auth = useAuth()
+ return useMemo(() => initializeQueryClient(auth), []) // eslint-disable-line react-hooks/exhaustive-deps
+}
diff --git a/opendc-web/opendc-web-ui/src/data/topology.js b/opendc-web/opendc-web-ui/src/data/topology.js
index bd4d1e4d..83abb6aa 100644
--- a/opendc-web/opendc-web-ui/src/data/topology.js
+++ b/opendc-web/opendc-web-ui/src/data/topology.js
@@ -46,7 +46,7 @@ export function configureTopologyClient(queryClient, auth) {
})
queryClient.setMutationDefaults('updateTopology', {
mutationFn: (data) => updateTopology(auth, data),
- onSuccess: async (result) => queryClient.setQueryData(['topologies', result._id], result),
+ onSuccess: (result) => queryClient.setQueryData(['topologies', result._id], result),
})
queryClient.setMutationDefaults('deleteTopology', {
mutationFn: (id) => deleteTopology(auth, id),
diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js
index d5f3b329..900ff405 100644
--- a/opendc-web/opendc-web-ui/src/pages/_app.js
+++ b/opendc-web/opendc-web-ui/src/pages/_app.js
@@ -23,15 +23,12 @@
import PropTypes from 'prop-types'
import Head from 'next/head'
import { Provider } from 'react-redux'
+import { useNewQueryClient } from '../data/query'
import { useStore } from '../redux'
-import { AuthProvider, useAuth, useRequireAuth } from '../auth'
+import { AuthProvider, useRequireAuth } from '../auth'
import * as Sentry from '@sentry/react'
import { Integrations } from '@sentry/tracing'
-import { QueryClient, QueryClientProvider } from 'react-query'
-import { useMemo } from 'react'
-import { configureProjectClient } from '../data/project'
-import { configureExperimentClient } from '../data/experiments'
-import { configureTopologyClient } from '../data/topology'
+import { QueryClientProvider } from 'react-query'
import '@patternfly/react-core/dist/styles/base.css'
import '@patternfly/react-styles/css/utilities/Alignment/alignment.css'
@@ -47,18 +44,12 @@ import '@patternfly/react-styles/css/components/InlineEdit/inline-edit.css'
import '../style/index.scss'
// This setup is necessary to forward the Auth0 context to the Redux context
-const Inner = ({ Component, pageProps }) => {
+function Inner({ Component, pageProps }) {
+ // Force user to be authorized
useRequireAuth()
- const auth = useAuth()
- const queryClient = useMemo(() => {
- const client = new QueryClient()
- configureProjectClient(client, auth)
- configureExperimentClient(client, auth)
- configureTopologyClient(client, auth)
- return client
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
- const store = useStore(pageProps.initialReduxState, { auth, queryClient })
+ const queryClient = useNewQueryClient()
+ const store = useStore(pageProps.initialReduxState, { queryClient })
return (
<QueryClientProvider client={queryClient}>
<Provider store={store}>
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js
deleted file mode 100644
index 9b4f8094..00000000
--- a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { call, put, select, getContext } from 'redux-saga/effects'
-import { fetchTopology, updateTopology } from '../../api/topologies'
-import { Topology } from '../../util/topology-schema'
-import { denormalize, normalize } from 'normalizr'
-import { storeTopology } from '../actions/topologies'
-
-/**
- * Fetches and normalizes the topology with the specified identifier.
- */
-export const fetchAndStoreTopology = function* (id) {
- const auth = yield getContext('auth')
-
- let topology = yield select((state) => state.objects.topology[id])
- if (!topology) {
- const newTopology = yield call(fetchTopology, auth, id)
- const { entities } = normalize(newTopology, Topology)
- yield put(storeTopology(entities))
- }
-
- return topology
-}
-
-export const updateTopologyOnServer = function* (id) {
- const topology = yield denormalizeTopology(id)
- const auth = yield getContext('auth')
- yield call(updateTopology, auth, topology)
-}
-
-/**
- * Denormalizes the topology representation in order to be stored on the server.
- */
-export const denormalizeTopology = function* (id) {
- const objects = yield select((state) => state.objects)
- const topology = objects.topology[id]
- return denormalize(topology, Topology, objects)
-}
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/query.js b/opendc-web/opendc-web-ui/src/redux/sagas/query.js
new file mode 100644
index 00000000..787006c7
--- /dev/null
+++ b/opendc-web/opendc-web-ui/src/redux/sagas/query.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { MutationObserver } from 'react-query'
+import { getContext, call } from 'redux-saga/effects'
+
+/**
+ * Fetch the query with the specified key.
+ */
+export function* fetchQuery(key, options) {
+ const queryClient = yield getContext('queryClient')
+ return yield call([queryClient, queryClient.fetchQuery], key, options)
+}
+
+/**
+ * Perform a mutation with the specified key.
+ */
+export function* mutate(key, object, options) {
+ const queryClient = yield getContext('queryClient')
+ const mutationObserver = new MutationObserver(queryClient, { mutationKey: key })
+ return yield call([mutationObserver, mutationObserver.mutate], object, options)
+}
diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
index a5a3be32..2d61643b 100644
--- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
+++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js
@@ -1,4 +1,6 @@
-import { call, put, select, getContext } from 'redux-saga/effects'
+import { normalize, denormalize } from 'normalizr'
+import { put, select } from 'redux-saga/effects'
+import { Topology } from '../../util/topology-schema'
import { goDownOneInteractionLevel } from '../actions/interaction-level'
import {
addIdToStoreObjectListProp,
@@ -6,6 +8,7 @@ import {
addToStore,
removeIdFromStoreObjectListProp,
} from '../actions/objects'
+import { storeTopology } from '../actions/topologies'
import {
cancelNewRoomConstructionSucceeded,
setCurrentTopology,
@@ -16,14 +19,15 @@ import {
DEFAULT_RACK_SLOT_CAPACITY,
MAX_NUM_UNITS_PER_MACHINE,
} from '../../components/topologies/map/MapConstants'
-import { fetchAndStoreTopology, denormalizeTopology, updateTopologyOnServer } from './objects'
import { uuid } from 'uuidv4'
-import { addTopology } from '../../api/topologies'
+import { fetchQuery, mutate } from './query'
+/**
+ * Fetches all topologies of the project with the specified identifier.
+ */
export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = false) {
try {
- const queryClient = yield getContext('queryClient')
- const project = yield call(() => queryClient.fetchQuery(['projects', projectId]))
+ const project = yield fetchQuery(['projects', projectId])
for (const id of project.topologyIds) {
yield fetchAndStoreTopology(id)
@@ -37,6 +41,37 @@ export function* fetchAndStoreAllTopologiesOfProject(projectId, setTopology = fa
}
}
+/**
+ * Fetches and normalizes the topology with the specified identifier.
+ */
+export function* fetchAndStoreTopology(id) {
+ let topology = yield select((state) => state.objects.topology[id])
+ if (!topology) {
+ const newTopology = yield fetchQuery(['topologies', id])
+ const { entities } = normalize(newTopology, Topology)
+ yield put(storeTopology(entities))
+ }
+
+ return topology
+}
+
+/**
+ * Synchronize the topology with the specified identifier with the server.
+ */
+export function* updateTopologyOnServer(id) {
+ const topology = yield denormalizeTopology(id)
+ yield mutate('updateTopology', topology)
+}
+
+/**
+ * Denormalizes the topology representation in order to be stored on the server.
+ */
+export function* denormalizeTopology(id) {
+ const objects = yield select((state) => state.objects)
+ const topology = objects.topology[id]
+ return denormalize(topology, Topology, objects)
+}
+
export function* onAddTopology({ projectId, duplicateId, name }) {
try {
let topologyToBeCreated
@@ -48,8 +83,7 @@ export function* onAddTopology({ projectId, duplicateId, name }) {
topologyToBeCreated = { name, rooms: [] }
}
- const auth = yield getContext('auth')
- const topology = yield call(addTopology, auth, { ...topologyToBeCreated, projectId })
+ const topology = yield mutate('addTopology', { ...topologyToBeCreated, projectId })
yield fetchAndStoreTopology(topology._id)
yield put(setCurrentTopology(topology._id))
} catch (error) {