From 1ce8bf170cda2afab334cd330325cd4fbb97dab4 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 11:46:57 +0200 Subject: ui: Split App container into separate components This change splits the App container into separate pages, as a starting point for removing much of the unnecessary state from Redux. --- opendc-web/opendc-web-ui/src/data/project.js | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'opendc-web/opendc-web-ui/src/data/project.js') diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index de2bc0d3..d4c95370 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -29,6 +29,13 @@ export function useProjects() { return useSelector((state) => state.projects) } +/** + * Return the project with the specified identifier. + */ +export function useProject(projectId) { + return useSelector((state) => state.projects[projectId]) +} + /** * Return the current active project. */ -- cgit v1.2.3 From aa788a3ad18badfac8beaabdaffc88b9e52f9306 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 15:07:11 +0200 Subject: ui: Remove current ids state from Redux This change removes the current active identifiers from the Redux state. Instead, we use the router query to track the active project, portfolio and topology. --- opendc-web/opendc-web-ui/src/data/project.js | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/data/project.js') diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index d4c95370..30b36efa 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -21,6 +21,7 @@ */ import { useSelector } from 'react-redux' +import { useRouter } from 'next/router' /** * Return the available projects. @@ -40,23 +41,9 @@ export function useProject(projectId) { * Return the current active project. */ export function useActiveProject() { - return useSelector((state) => - state.currentProjectId !== '-1' ? state.objects.project[state.currentProjectId] : undefined - ) -} - -/** - * Return the active portfolio. - */ -export function useActivePortfolio() { - return useSelector((state) => state.objects.portfolio[state.currentPortfolioId]) -} - -/** - * Return the active scenario. - */ -export function useActiveScenario() { - return useSelector((state) => state.objects.scenario[state.currentScenarioId]) + const router = useRouter() + const { project: projectId } = router.query + return useSelector((state) => state.objects.project[projectId]) } /** -- cgit v1.2.3 From e5e5d2c65e583493870bc0b62fb185c5e757c13f Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 16:27:49 +0200 Subject: ui: Migrate project APIs to React Query This change updates the OpenDC frontend to use React Query for fetching and mutating project data. Previously, this state was tracked and synchronized via Redux. Migrating to React Query greatly simplifies the state synchronization logic necessary in the frontend. --- opendc-web/opendc-web-ui/src/data/project.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/data/project.js') diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index 30b36efa..308930e5 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -21,39 +21,43 @@ */ import { useSelector } from 'react-redux' +import { useQuery } from 'react-query' +import { fetchProject, fetchProjects } from '../api/projects' +import { useAuth } from '../auth' import { useRouter } from 'next/router' /** * Return the available projects. */ export function useProjects() { - return useSelector((state) => state.projects) + const auth = useAuth() + return useQuery('projects', () => fetchProjects(auth)) } /** * Return the project with the specified identifier. */ export function useProject(projectId) { - return useSelector((state) => state.projects[projectId]) + const auth = useAuth() + return useQuery(`projects/${projectId}`, () => fetchProject(auth, projectId), { enabled: !!projectId }) } /** - * Return the current active project. + * Return the current active project identifier. */ -export function useActiveProject() { +export function useActiveProjectId() { const router = useRouter() - const { project: projectId } = router.query - return useSelector((state) => state.objects.project[projectId]) + const { project } = router.query + return project } /** * Return the portfolios for the specified project id. */ export function usePortfolios(projectId) { + const { data: project } = useProject(projectId) return useSelector((state) => { - let portfolios = state.objects.project[projectId] - ? state.objects.project[projectId].portfolioIds.map((t) => state.objects.portfolio[t]) - : [] + let portfolios = project?.portfolioIds?.map((t) => state.objects.portfolio[t]) ?? [] if (portfolios.filter((t) => !t).length > 0) { portfolios = [] } -- cgit v1.2.3 From 9c8a987556d0fb0cdf0eb67e0c191a8dcc5593b9 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 17:30:15 +0200 Subject: ui: Fetch scenarios and portfolios using React Query --- opendc-web/opendc-web-ui/src/data/project.js | 62 +++++++++++++++------------- 1 file changed, 33 insertions(+), 29 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/data/project.js') diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index 308930e5..5cf620da 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -20,11 +20,12 @@ * SOFTWARE. */ -import { useSelector } from 'react-redux' -import { useQuery } from 'react-query' +import { useQueries, useQuery } from 'react-query' import { fetchProject, fetchProjects } from '../api/projects' import { useAuth } from '../auth' import { useRouter } from 'next/router' +import { fetchPortfolio } from '../api/portfolios' +import { fetchScenario } from '../api/scenarios' /** * Return the available projects. @@ -39,45 +40,48 @@ export function useProjects() { */ export function useProject(projectId) { const auth = useAuth() - return useQuery(`projects/${projectId}`, () => fetchProject(auth, projectId), { enabled: !!projectId }) + return useQuery(['projects', projectId], () => fetchProject(auth, projectId), { enabled: !!projectId }) } /** - * Return the current active project identifier. + * Return the portfolio with the specified identifier. */ -export function useActiveProjectId() { - const router = useRouter() - const { project } = router.query - return project +export function usePortfolio(portfolioId) { + const auth = useAuth() + return useQuery(['portfolios', portfolioId], () => fetchPortfolio(auth, portfolioId), { enabled: !!portfolioId }) } /** * Return the portfolios for the specified project id. */ -export function usePortfolios(projectId) { - const { data: project } = useProject(projectId) - return useSelector((state) => { - let portfolios = project?.portfolioIds?.map((t) => state.objects.portfolio[t]) ?? [] - if (portfolios.filter((t) => !t).length > 0) { - portfolios = [] - } - - return portfolios - }) +export function usePortfolios(portfolioIds) { + const auth = useAuth() + return useQueries( + portfolioIds.map((portfolioId) => ({ + queryKey: ['portfolios', portfolioId], + queryFn: () => fetchPortfolio(auth, portfolioId), + })) + ) } /** - * Return the scenarios for the specified portfolio id. + * Return the scenarios with the specified identifiers. */ -export function useScenarios(portfolioId) { - return useSelector((state) => { - let scenarios = state.objects.portfolio[portfolioId] - ? state.objects.portfolio[portfolioId].scenarioIds.map((t) => state.objects.scenario[t]) - : [] - if (scenarios.filter((t) => !t).length > 0) { - scenarios = [] - } +export function useScenarios(scenarioIds) { + const auth = useAuth() + return useQueries( + scenarioIds.map((scenarioId) => ({ + queryKey: ['scenario', scenarioId], + queryFn: () => fetchScenario(auth, scenarioId), + })) + ) +} - return scenarios - }) +/** + * Return the current active project identifier. + */ +export function useActiveProjectId() { + const router = useRouter() + const { project } = router.query + return project } -- cgit v1.2.3 From 02a2f0f89cb1f39a5f8856bca1971a4e1b12374f Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Wed, 7 Jul 2021 20:13:30 +0200 Subject: ui: Use React Query defaults to reduce duplication --- opendc-web/opendc-web-ui/src/data/project.js | 96 +++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 15 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/data/project.js') diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index 5cf620da..256203a3 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -21,45 +21,113 @@ */ import { useQueries, useQuery } from 'react-query' -import { fetchProject, fetchProjects } from '../api/projects' -import { useAuth } from '../auth' +import { addProject, deleteProject, fetchProject, fetchProjects } from '../api/projects' import { useRouter } from 'next/router' -import { fetchPortfolio } from '../api/portfolios' -import { fetchScenario } from '../api/scenarios' +import { addPortfolio, deletePortfolio, fetchPortfolio } from '../api/portfolios' +import { addScenario, deleteScenario, fetchScenario } from '../api/scenarios' + +/** + * Configure the query defaults for the project endpoints. + */ +export function configureProjectClient(queryClient, auth) { + queryClient.setQueryDefaults('projects', { + queryFn: ({ queryKey }) => (queryKey.length === 1 ? fetchProjects(auth) : fetchProject(auth, queryKey[1])), + }) + + queryClient.setMutationDefaults('addProject', { + mutationFn: (data) => addProject(auth, data), + onSuccess: async (result) => { + queryClient.setQueryData('projects', (old = []) => [...old, result]) + }, + }) + queryClient.setMutationDefaults('deleteProject', { + mutationFn: (id) => deleteProject(auth, id), + onSuccess: async (result) => { + queryClient.setQueryData('projects', (old = []) => old.filter((project) => project._id !== result._id)) + queryClient.removeQueries(['projects', result._id]) + }, + }) + + queryClient.setQueryDefaults('portfolios', { + queryFn: ({ queryKey }) => fetchPortfolio(auth, queryKey[1]), + }) + queryClient.setMutationDefaults('addPortfolio', { + mutationFn: (data) => addPortfolio(auth, data), + onSuccess: async (result) => { + queryClient.setQueryData(['projects', result.projectId], (old) => ({ + ...old, + portfolioIds: [...old.portfolioIds, result._id], + })) + queryClient.setQueryData(['portfolios', result._id], result) + }, + }) + queryClient.setMutationDefaults('deletePortfolio', { + mutationFn: (id) => deletePortfolio(auth, id), + onSuccess: async (result) => { + queryClient.setQueryData(['projects', result.projectId], (old) => ({ + ...old, + portfolioIds: old.portfolioIds.filter((id) => id !== result._id), + })) + queryClient.removeQueries(['portfolios', result._id]) + }, + }) + + queryClient.setQueryDefaults('scenarios', { + queryFn: ({ queryKey }) => fetchScenario(auth, queryKey[1]), + }) + queryClient.setMutationDefaults('addScenario', { + mutationFn: (data) => addScenario(auth, data), + onSuccess: async (result) => { + // Register updated scenario in cache + queryClient.setQueryData(['scenarios', result._id], result) + + // Add scenario id to portfolio + queryClient.setQueryData(['portfolios', result.portfolioId], (old) => ({ + ...old, + scenarioIds: [...old.scenarioIds, result._id], + })) + }, + }) + queryClient.setMutationDefaults('deleteScenario', { + mutationFn: (id) => deleteScenario(auth, id), + onSuccess: async (result) => { + queryClient.setQueryData(['portfolios', result.portfolioId], (old) => ({ + ...old, + scenarioIds: old.scenarioIds.filter((id) => id !== result._id), + })) + queryClient.removeQueries(['scenarios', result._id]) + }, + }) +} /** * Return the available projects. */ export function useProjects() { - const auth = useAuth() - return useQuery('projects', () => fetchProjects(auth)) + return useQuery('projects') } /** * Return the project with the specified identifier. */ export function useProject(projectId) { - const auth = useAuth() - return useQuery(['projects', projectId], () => fetchProject(auth, projectId), { enabled: !!projectId }) + return useQuery(['projects', projectId], { enabled: !!projectId }) } /** * Return the portfolio with the specified identifier. */ export function usePortfolio(portfolioId) { - const auth = useAuth() - return useQuery(['portfolios', portfolioId], () => fetchPortfolio(auth, portfolioId), { enabled: !!portfolioId }) + return useQuery(['portfolios', portfolioId], { enabled: !!portfolioId }) } /** * Return the portfolios for the specified project id. */ export function usePortfolios(portfolioIds) { - const auth = useAuth() return useQueries( portfolioIds.map((portfolioId) => ({ queryKey: ['portfolios', portfolioId], - queryFn: () => fetchPortfolio(auth, portfolioId), })) ) } @@ -68,11 +136,9 @@ export function usePortfolios(portfolioIds) { * Return the scenarios with the specified identifiers. */ export function useScenarios(scenarioIds) { - const auth = useAuth() return useQueries( scenarioIds.map((scenarioId) => ({ - queryKey: ['scenario', scenarioId], - queryFn: () => fetchScenario(auth, scenarioId), + queryKey: ['scenarios', scenarioId], })) ) } -- cgit v1.2.3 From 2c8d675c2cf140eac05988065a9d20fd2773399a Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Thu, 8 Jul 2021 13:36:39 +0200 Subject: ui: Combine fetching of project relations This change updates the OpenDC frontend to combine the fetching of project relations. This means that for a single project, we make only one additional request to retrieve all its topologies. --- opendc-web/opendc-web-ui/src/data/project.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) (limited to 'opendc-web/opendc-web-ui/src/data/project.js') diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index 256203a3..9bdcfb93 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -23,8 +23,8 @@ import { useQueries, useQuery } from 'react-query' import { addProject, deleteProject, fetchProject, fetchProjects } from '../api/projects' import { useRouter } from 'next/router' -import { addPortfolio, deletePortfolio, fetchPortfolio } from '../api/portfolios' -import { addScenario, deleteScenario, fetchScenario } from '../api/scenarios' +import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfoliosOfProject } from '../api/portfolios' +import { addScenario, deleteScenario, fetchScenario, fetchScenariosOfPortfolio } from '../api/scenarios' /** * Configure the query defaults for the project endpoints. @@ -51,6 +51,9 @@ export function configureProjectClient(queryClient, auth) { queryClient.setQueryDefaults('portfolios', { queryFn: ({ queryKey }) => fetchPortfolio(auth, queryKey[1]), }) + queryClient.setQueryDefaults('project-portfolios', { + queryFn: ({ queryKey }) => fetchPortfoliosOfProject(auth, queryKey[1]), + }) queryClient.setMutationDefaults('addPortfolio', { mutationFn: (data) => addPortfolio(auth, data), onSuccess: async (result) => { @@ -75,6 +78,9 @@ export function configureProjectClient(queryClient, auth) { queryClient.setQueryDefaults('scenarios', { queryFn: ({ queryKey }) => fetchScenario(auth, queryKey[1]), }) + queryClient.setQueryDefaults('portfolio-scenarios', { + queryFn: ({ queryKey }) => fetchScenariosOfPortfolio(auth, queryKey[1]), + }) queryClient.setMutationDefaults('addScenario', { mutationFn: (data) => addScenario(auth, data), onSuccess: async (result) => { @@ -122,14 +128,10 @@ export function usePortfolio(portfolioId) { } /** - * Return the portfolios for the specified project id. + * Return the portfolios of the specified project. */ -export function usePortfolios(portfolioIds) { - return useQueries( - portfolioIds.map((portfolioId) => ({ - queryKey: ['portfolios', portfolioId], - })) - ) +export function useProjectPortfolios(projectId) { + return useQuery(['project-portfolios', projectId], { enabled: !!projectId }) } /** @@ -143,6 +145,13 @@ export function useScenarios(scenarioIds) { ) } +/** + * Return the scenarios of the specified portfolio. + */ +export function usePortfolioScenarios(portfolioId) { + return useQuery(['portfolio-scenarios', portfolioId], { enabled: !!portfolioId }) +} + /** * Return the current active project identifier. */ -- cgit v1.2.3