diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-07-20 14:09:39 +0200 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-07-20 14:09:39 +0200 |
| commit | 6e3ad713111f35fc58bd2b7f1be5aeeb57eb94a8 (patch) | |
| tree | bcd0466cae9792be5d594ca821d54e843b006423 /opendc-web/opendc-web-ui | |
| parent | 51c759e74b088d405b63fdb3e374822308d21366 (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.
Diffstat (limited to 'opendc-web/opendc-web-ui')
| -rw-r--r-- | opendc-web/opendc-web-ui/src/data/query.js | 57 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/data/topology.js | 2 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/pages/_app.js | 23 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/redux/sagas/objects.js | 36 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/redux/sagas/query.js | 41 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/redux/sagas/topology.js | 48 |
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) { |
