From c4373cce465fde5eb8fa2873ac2f806a83c0bce1 Mon Sep 17 00:00:00 2001 From: William Petit Date: Tue, 21 Jul 2020 22:25:39 +0200 Subject: [PATCH] Remplacement de Redux/Saga par Apollo --- client/package-lock.json | 173 ++++-------------- client/package.json | 12 +- client/src/components/App.tsx | 18 +- client/src/components/HomePage/Dashboard.tsx | 5 +- client/src/components/HomePage/HomePage.tsx | 20 +- .../components/HomePage/WorkgroupsPanel.tsx | 41 +++-- client/src/components/Navbar.tsx | 46 ++--- .../components/ProfilePage/ProfilePage.tsx | 23 ++- client/src/components/UserForm.tsx | 10 +- client/src/components/WithLoader.tsx | 22 +++ client/src/gql/client.tsx | 21 +++ client/src/gql/mutations/profile.tsx | 15 ++ client/src/gql/queries/profile.tsx | 16 ++ client/src/gql/queries/workgroups.tsx | 20 ++ client/src/index.tsx | 7 +- client/src/sass/_all.scss | 1 - client/src/store/actions/auth.ts | 11 -- client/src/store/actions/profile.ts | 40 ---- client/src/store/actions/workgroups.ts | 19 -- client/src/store/reducers/auth.ts | 58 ------ client/src/store/reducers/flags.ts | 32 ---- client/src/store/reducers/root.ts | 16 -- client/src/store/reducers/workgroups.ts | 38 ---- client/src/store/sagas/failure.ts | 21 --- client/src/store/sagas/init.ts | 12 -- client/src/store/sagas/profile.ts | 46 ----- client/src/store/sagas/root.ts | 14 -- client/src/store/sagas/workgroups.ts | 25 --- client/src/store/selectors/flags.ts | 7 - client/src/store/store.ts | 30 --- client/src/util/daddy.ts | 134 -------------- internal/route/mount.go | 1 - internal/session/user_email.go | 4 +- 33 files changed, 230 insertions(+), 728 deletions(-) create mode 100644 client/src/components/WithLoader.tsx create mode 100644 client/src/gql/client.tsx create mode 100644 client/src/gql/mutations/profile.tsx create mode 100644 client/src/gql/queries/profile.tsx create mode 100644 client/src/gql/queries/workgroups.tsx delete mode 100644 client/src/store/actions/auth.ts delete mode 100644 client/src/store/actions/profile.ts delete mode 100644 client/src/store/actions/workgroups.ts delete mode 100644 client/src/store/reducers/auth.ts delete mode 100644 client/src/store/reducers/flags.ts delete mode 100644 client/src/store/reducers/root.ts delete mode 100644 client/src/store/reducers/workgroups.ts delete mode 100644 client/src/store/sagas/failure.ts delete mode 100644 client/src/store/sagas/init.ts delete mode 100644 client/src/store/sagas/profile.ts delete mode 100644 client/src/store/sagas/root.ts delete mode 100644 client/src/store/sagas/workgroups.ts delete mode 100644 client/src/store/selectors/flags.ts delete mode 100644 client/src/store/store.ts delete mode 100644 client/src/util/daddy.ts diff --git a/client/package-lock.json b/client/package-lock.json index e23fc13..8552a0f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4,6 +4,43 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@apollo/client": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.0.2.tgz", + "integrity": "sha512-4ighan5Anlj4tK/tdUHs4Mi1njqXZ7AxRCVolz/H702DjPphAJfm+FRkIadPTmwz+OLO+d+tX+6V1VBshf02rg==", + "requires": { + "@types/zen-observable": "^0.8.0", + "@wry/context": "^0.5.2", + "@wry/equality": "^0.1.9", + "fast-json-stable-stringify": "^2.0.0", + "graphql-tag": "^2.10.4", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.12.1", + "prop-types": "^15.7.2", + "symbol-observable": "^1.2.0", + "ts-invariant": "^0.4.4", + "tslib": "^1.10.0", + "zen-observable": "^0.8.14" + }, + "dependencies": { + "@wry/context": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.5.2.tgz", + "integrity": "sha512-B/JLuRZ/vbEKHRUiGj6xiMojST1kHhu4WcreLfNN7q9DqQFrb97cWgf/kiYsPSUCAMVN0HzfFc8XjJdzgZzfjw==", + "requires": { + "tslib": "^1.9.3" + } + }, + "optimism": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.12.1.tgz", + "integrity": "sha512-t8I7HM1dw0SECitBYAqFOVHoBAHEQBTeKjIL9y9ImHzAVkdyPK4ifTgM4VJRDtTUY4r/u5Eqxs4XcGPHaoPkeQ==", + "requires": { + "@wry/context": "^0.5.2" + } + } + } + }, "@babel/code-frame": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", @@ -1230,7 +1267,8 @@ "@types/node": { "version": "13.13.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.12.tgz", - "integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw==" + "integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw==", + "dev": true }, "@types/prop-types": { "version": "15.7.3", @@ -1551,15 +1589,6 @@ "@xtuc/long": "4.2.2" } }, - "@wry/context": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz", - "integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==", - "requires": { - "@types/node": ">=6", - "tslib": "^1.9.3" - } - }, "@wry/equality": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz", @@ -1744,93 +1773,6 @@ } } }, - "apollo-cache": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.3.5.tgz", - "integrity": "sha512-1XoDy8kJnyWY/i/+gLTEbYLnoiVtS8y7ikBr/IfmML4Qb+CM7dEEbIUOjnY716WqmZ/UpXIxTfJsY7rMcqiCXA==", - "requires": { - "apollo-utilities": "^1.3.4", - "tslib": "^1.10.0" - } - }, - "apollo-cache-inmemory": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.6.tgz", - "integrity": "sha512-L8pToTW/+Xru2FFAhkZ1OA9q4V4nuvfoPecBM34DecAugUZEBhI2Hmpgnzq2hTKZ60LAMrlqiASm0aqAY6F8/A==", - "requires": { - "apollo-cache": "^1.3.5", - "apollo-utilities": "^1.3.4", - "optimism": "^0.10.0", - "ts-invariant": "^0.4.0", - "tslib": "^1.10.0" - } - }, - "apollo-client": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.6.10.tgz", - "integrity": "sha512-jiPlMTN6/5CjZpJOkGeUV0mb4zxx33uXWdj/xQCfAMkuNAC3HN7CvYDyMHHEzmcQ5GV12LszWoQ/VlxET24CtA==", - "requires": { - "@types/zen-observable": "^0.8.0", - "apollo-cache": "1.3.5", - "apollo-link": "^1.0.0", - "apollo-utilities": "1.3.4", - "symbol-observable": "^1.0.2", - "ts-invariant": "^0.4.0", - "tslib": "^1.10.0", - "zen-observable": "^0.8.0" - } - }, - "apollo-link": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz", - "integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==", - "requires": { - "apollo-utilities": "^1.3.0", - "ts-invariant": "^0.4.0", - "tslib": "^1.9.3", - "zen-observable-ts": "^0.8.21" - } - }, - "apollo-link-http": { - "version": "1.5.17", - "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz", - "integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==", - "requires": { - "apollo-link": "^1.2.14", - "apollo-link-http-common": "^0.2.16", - "tslib": "^1.9.3" - } - }, - "apollo-link-http-common": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz", - "integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==", - "requires": { - "apollo-link": "^1.2.14", - "ts-invariant": "^0.4.0", - "tslib": "^1.9.3" - } - }, - "apollo-link-ws": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/apollo-link-ws/-/apollo-link-ws-1.0.20.tgz", - "integrity": "sha512-mjSFPlQxmoLArpHBeUb2Xj+2HDYeTaJqFGOqQ+I8NVJxgL9lJe84PDWcPah/yMLv3rB7QgBDSuZ0xoRFBPlySw==", - "requires": { - "apollo-link": "^1.2.14", - "tslib": "^1.9.3" - } - }, - "apollo-utilities": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz", - "integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==", - "requires": { - "@wry/equality": "^0.1.2", - "fast-json-stable-stringify": "^2.0.0", - "ts-invariant": "^0.4.0", - "tslib": "^1.10.0" - } - }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -3239,11 +3181,6 @@ "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz", "integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ==" }, - "bulma-switch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.0.tgz", - "integrity": "sha512-myD38zeUfjmdduq+pXabhJEe3x2hQP48l/OI+Y0fO3HdDynZUY/VJygucvEAJKRjr4HxD5DnEm4yx+oDOBXpAA==" - }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -5615,11 +5552,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz", "integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==" }, - "graphql-request": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-2.0.0.tgz", - "integrity": "sha512-Ww3Ax+G3l2d+mPT8w7HC9LfrKjutnCKtnDq7ZZp2ghVk5IQDjwAk3/arRF1ix17Ky15rm0hrSKVKxRhIVlSuoQ==" - }, "graphql-tag": { "version": "2.10.4", "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.4.tgz", @@ -6507,11 +6439,6 @@ "verror": "1.10.0" } }, - "jwt-decode": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", - "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" - }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -7466,14 +7393,6 @@ "is-wsl": "^1.1.0" } }, - "optimism": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz", - "integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==", - "requires": { - "@wry/context": "^0.4.0" - } - }, "original": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", @@ -8048,11 +7967,6 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, - "qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" - }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -11006,15 +10920,6 @@ "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" - }, - "zen-observable-ts": { - "version": "0.8.21", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz", - "integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==", - "requires": { - "tslib": "^1.9.3", - "zen-observable": "^0.8.0" - } } } } diff --git a/client/package.json b/client/package.json index a5edbbc..ad76110 100644 --- a/client/package.json +++ b/client/package.json @@ -51,20 +51,10 @@ "webpack-dev-server": "^3.11.0" }, "dependencies": { + "@apollo/client": "^3.0.2", "@types/qs": "^6.9.3", - "apollo-cache-inmemory": "^1.6.6", - "apollo-client": "^2.6.10", - "apollo-link": "^1.2.14", - "apollo-link-http": "^1.5.17", - "apollo-link-ws": "^1.0.20", - "apollo-utilities": "^1.3.4", "bulma": "^0.9.0", - "bulma-switch": "^2.0.0", "graphql": "^15.3.0", - "graphql-request": "^2.0.0", - "graphql-tag": "^2.10.4", - "jwt-decode": "^2.2.0", - "qs": "^6.9.4", "react": "^16.12.0", "react-dom": "^16.12.0", "react-redux": "^7.1.3", diff --git a/client/src/components/App.tsx b/client/src/components/App.tsx index ec0367f..af9e243 100644 --- a/client/src/components/App.tsx +++ b/client/src/components/App.tsx @@ -1,22 +1,18 @@ import React from 'react'; import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom"; import { HomePage } from './HomePage/HomePage'; -import { store } from '../store/store'; -import { Provider } from 'react-redux'; import { ProfilePage } from './ProfilePage/ProfilePage'; export class App extends React.Component { render() { return ( - - - - - - } /> - - - + + + + + } /> + + ); } } \ No newline at end of file diff --git a/client/src/components/HomePage/Dashboard.tsx b/client/src/components/HomePage/Dashboard.tsx index 925ae03..916fba1 100644 --- a/client/src/components/HomePage/Dashboard.tsx +++ b/client/src/components/HomePage/Dashboard.tsx @@ -1,7 +1,4 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { fetchWorkgroups } from '../../store/actions/workgroups'; -import { RootState } from '../../store/reducers/root'; +import React from 'react'; import { WorkgroupsPanel } from './WorkgroupsPanel'; export function Dashboard() { diff --git a/client/src/components/HomePage/HomePage.tsx b/client/src/components/HomePage/HomePage.tsx index 5521d49..bd01c65 100644 --- a/client/src/components/HomePage/HomePage.tsx +++ b/client/src/components/HomePage/HomePage.tsx @@ -1,18 +1,26 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Page } from '../Page'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../store/reducers/root'; import { Dashboard } from './Dashboard'; +import { useUserProfileQuery } from '../../gql/queries/profile'; +import { Loader } from '../Loader'; export function HomePage() { - const currentUser = useSelector((state: RootState) => state.auth.currentUser); + const { data, loading } = useUserProfileQuery(); + + if (loading) { + return ( + + ); + } + + const { userProfile } = (data || {}); return ( - +
{ - currentUser ? + userProfile ? :
diff --git a/client/src/components/HomePage/WorkgroupsPanel.tsx b/client/src/components/HomePage/WorkgroupsPanel.tsx index 5e6f48a..6648bc0 100644 --- a/client/src/components/HomePage/WorkgroupsPanel.tsx +++ b/client/src/components/HomePage/WorkgroupsPanel.tsx @@ -1,44 +1,49 @@ import React, { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '../../store/reducers/root'; -import { fetchWorkgroups } from '../../store/actions/workgroups'; import { Workgroup } from '../../types/workgroup'; import { User } from '../../types/user'; import { Link } from 'react-router-dom'; +import { useWorkgroupsQuery } from '../../gql/queries/workgroups'; +import { useUserProfileQuery } from '../../gql/queries/profile'; +import { Loader } from '../Loader'; export function WorkgroupsPanel() { - const dispatch = useDispatch(); - const workgroups = useSelector(state => state.workgroups.workgroupsById); - const currentUserId = useSelector(state => state.auth.currentUser.id); + const workgroupsQuery = useWorkgroupsQuery(); + const userProfileQuery = useUserProfileQuery(); + const [ state, setState ] = useState({ selectedTab: 0 }); + + const isLoading = userProfileQuery.loading || workgroupsQuery.loading; + if (isLoading) { + return ; + } + + let { data: { userProfile }} = userProfileQuery; + let { data: { workgroups }} = workgroupsQuery; const filterTabs = [ { label: "Mes groupes", - filter: workgroups => Object.values(workgroups).filter((wg: Workgroup) => { - return wg.members.some((u: User) => u.id === currentUserId); + filter: workgroups => workgroups.filter((wg: Workgroup) => { + return wg.members.some((u: User) => u.id === userProfile.id); }) }, { label: "Ouverts", - filter: workgroups => Object.values(workgroups).filter((wg: Workgroup) => !wg.closedAt) + filter: workgroups => workgroups.filter((wg: Workgroup) => !wg.closedAt) }, { label: "Clôs", - filter: workgroups => Object.values(workgroups).filter((wg: Workgroup) => !!wg.closedAt) + filter: workgroups => workgroups.filter((wg: Workgroup) => !!wg.closedAt) } ]; - const [ state, setState ] = useState({ selectedTab: 0 }); - const selectTab = (tabIndex: number) => { setState(state => ({ ...state, selectedTab: tabIndex })); } + - useEffect(() => { - dispatch(fetchWorkgroups()); - }, []); - - const workgroupsItems = filterTabs[state.selectedTab].filter(workgroups).map((wg: Workgroup) => { + let workgroupsItems = []; + + workgroupsItems = filterTabs[state.selectedTab].filter(workgroups).map((wg: Workgroup) => { return ( @@ -47,7 +52,7 @@ export function WorkgroupsPanel() { {wg.name} ); - }) + }); return (
diff --git a/client/src/components/UserForm.tsx b/client/src/components/UserForm.tsx index 1ba6c7a..8e94f4e 100644 --- a/client/src/components/UserForm.tsx +++ b/client/src/components/UserForm.tsx @@ -10,11 +10,11 @@ export function UserForm({ user, onChange }: UserFormProps) { const [ state, setState ] = useState({ changed: false, user: { - name: '', - email: '', - createdAt: null, - connectedAt: null, - ...user, + id: user && user.id ? user.id : '', + name: user && user.name ? user.name : '', + email: user && user.email ? user.email : '', + createdAt: user && user.createdAt ? user.createdAt : null, + connectedAt: user && user.connectedAt ? user.connectedAt : null, } }); diff --git a/client/src/components/WithLoader.tsx b/client/src/components/WithLoader.tsx new file mode 100644 index 0000000..33f5441 --- /dev/null +++ b/client/src/components/WithLoader.tsx @@ -0,0 +1,22 @@ +import React, { Fragment, PropsWithChildren, FunctionComponent } from 'react'; + +export interface WithLoaderProps { + loading?: boolean +} + +export const WithLoader: FunctionComponent = ({ loading, children }) => { + return ( + + { + loading ? +
+
+
+
+
+
: + children + } +
+ ) +} \ No newline at end of file diff --git a/client/src/gql/client.tsx b/client/src/gql/client.tsx new file mode 100644 index 0000000..bbe3c37 --- /dev/null +++ b/client/src/gql/client.tsx @@ -0,0 +1,21 @@ +import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'; +import { Config } from '../config'; +import { WebSocketLink } from "@apollo/client/link/ws"; +import { RetryLink } from "@apollo/client/link/retry"; +import { SubscriptionClient } from "subscriptions-transport-ws"; + + +const subscriptionClient = new SubscriptionClient(Config.subscriptionEndpoint, { + reconnect: true +}); + +const link = new RetryLink().split( + (operation) => operation.operationName === 'subscription', + new WebSocketLink(subscriptionClient), + new HttpLink({ uri: Config.graphQLEndpoint, credentials: 'include' }) +); + +export const client = new ApolloClient({ + cache: new InMemoryCache(), + link: link, +}); \ No newline at end of file diff --git a/client/src/gql/mutations/profile.tsx b/client/src/gql/mutations/profile.tsx new file mode 100644 index 0000000..9a681a4 --- /dev/null +++ b/client/src/gql/mutations/profile.tsx @@ -0,0 +1,15 @@ +import { gql, useQuery, useMutation } from '@apollo/client'; + +const MUTATION_UPDATE_USER_PROFILE = gql` +mutation updateUserProfile($changes: ProfileChanges!) { + updateProfile(changes: $changes) { + id, + name, + createdAt, + connectedAt, + } +}`; + +export function useUpdateUserProfileMutation() { + return useMutation(MUTATION_UPDATE_USER_PROFILE); +} \ No newline at end of file diff --git a/client/src/gql/queries/profile.tsx b/client/src/gql/queries/profile.tsx new file mode 100644 index 0000000..128c394 --- /dev/null +++ b/client/src/gql/queries/profile.tsx @@ -0,0 +1,16 @@ +import { gql, useQuery } from '@apollo/client'; + +const QUERY_USER_PROFILE = gql` +query userProfile { + userProfile { + id, + name, + email, + createdAt, + connectedAt + } +}`; + +export function useUserProfileQuery() { + return useQuery(QUERY_USER_PROFILE, { fetchPolicy: "network-only" }); +} \ No newline at end of file diff --git a/client/src/gql/queries/workgroups.tsx b/client/src/gql/queries/workgroups.tsx new file mode 100644 index 0000000..e8c7d7a --- /dev/null +++ b/client/src/gql/queries/workgroups.tsx @@ -0,0 +1,20 @@ +import { gql, useQuery } from '@apollo/client'; + +const QUERY_WORKGROUP = gql` + query workgroups { + workgroups { + id, + name, + createdAt, + closedAt, + members { + id, + email + } + } + } +`; + +export function useWorkgroupsQuery(options = {}) { + return useQuery(QUERY_WORKGROUP, options); +} \ No newline at end of file diff --git a/client/src/index.tsx b/client/src/index.tsx index 9dde128..cee36c1 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -2,15 +2,18 @@ import './sass/_all.scss'; import React from 'react'; import ReactDOM from 'react-dom'; import { App } from './components/App'; -import { Config } from './config'; +import { client } from './gql/client'; import '@fortawesome/fontawesome-free/js/fontawesome' import '@fortawesome/fontawesome-free/js/solid' import '@fortawesome/fontawesome-free/js/regular' import '@fortawesome/fontawesome-free/js/brands' import './resources/favicon.png'; +import { ApolloProvider } from '@apollo/client'; ReactDOM.render( - , + + + , document.getElementById('app') ); diff --git a/client/src/sass/_all.scss b/client/src/sass/_all.scss index 8f13fd0..c08fa8c 100644 --- a/client/src/sass/_all.scss +++ b/client/src/sass/_all.scss @@ -1,4 +1,3 @@ @import 'bulma/bulma.sass'; -@import 'bulma-switch/dist/css/bulma-switch.sass'; @import '_base.scss'; @import '_loader.scss'; \ No newline at end of file diff --git a/client/src/store/actions/auth.ts b/client/src/store/actions/auth.ts deleted file mode 100644 index 8a8c608..0000000 --- a/client/src/store/actions/auth.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Action } from "redux"; - -export const SET_CURRENT_USER = 'SET_CURRENT_USER'; - -export interface setCurrentUserAction extends Action { - email: string -} - -export function setCurrentUser(email: string): setCurrentUserAction { - return { type: SET_CURRENT_USER, email }; -} \ No newline at end of file diff --git a/client/src/store/actions/profile.ts b/client/src/store/actions/profile.ts deleted file mode 100644 index 8e51900..0000000 --- a/client/src/store/actions/profile.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Action } from "redux"; -import { User } from "../../types/user"; - -export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST'; -export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS'; -export const FETCH_PROFILE_FAILURE = 'FETCH_PROFILE_FAILURE'; - -export interface fetchProfileRequestAction extends Action { - -} - -export interface fetchProfileSuccessAction extends Action { - profile: User -} - - -export function fetchProfile(): fetchProfileRequestAction { - return { type: FETCH_PROFILE_REQUEST } -} - -export const UPDATE_PROFILE_REQUEST = 'UPDATE_PROFILE_REQUEST'; -export const UPDATE_PROFILE_SUCCESS = 'UPDATE_PROFILE_SUCCESS'; -export const UPDATE_PROFILE_FAILURE = 'UPDATE_PROFILE_FAILURE'; - -export interface ProfileChanges { - name?: string -} - -export interface updateProfileRequestAction extends Action { - changes: ProfileChanges -} - -export interface updateProfileSuccessAction extends Action { - profile: User -} - - -export function updateProfile(changes: ProfileChanges): updateProfileRequestAction { - return { type: UPDATE_PROFILE_REQUEST, changes } -} \ No newline at end of file diff --git a/client/src/store/actions/workgroups.ts b/client/src/store/actions/workgroups.ts deleted file mode 100644 index 6894390..0000000 --- a/client/src/store/actions/workgroups.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Action } from "redux"; -import { Workgroup } from "../../types/workgroup"; - -export const FETCH_WORKGROUPS_REQUEST = 'FETCH_WORKGROUPS_REQUEST'; -export const FETCH_WORKGROUPS_SUCCESS = 'FETCH_WORKGROUPS_SUCCESS'; -export const FETCH_WORKGROUPS_FAILURE = 'FETCH_WORKGROUPS_FAILURE'; - -export interface fetchWorkgroupsRequestAction extends Action { - -} - -export interface fetchWorkgroupsSuccessAction extends Action { - workgroups: [Workgroup] -} - - -export function fetchWorkgroups(): fetchWorkgroupsRequestAction { - return { type: FETCH_WORKGROUPS_REQUEST } -} \ No newline at end of file diff --git a/client/src/store/reducers/auth.ts b/client/src/store/reducers/auth.ts deleted file mode 100644 index c37ba47..0000000 --- a/client/src/store/reducers/auth.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Action } from "redux"; -import { User } from "../../types/user"; -import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth"; -import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction, updateProfileSuccessAction, UPDATE_PROFILE_SUCCESS, updateProfileRequestAction } from "../actions/profile"; - -export interface AuthState { - isAuthenticated: boolean - currentUser: User -} - -const defaultState = { - isAuthenticated: false, - currentUser: null, -}; - -export function authReducer(state = defaultState, action: Action): AuthState { - switch (action.type) { - case SET_CURRENT_USER: - return handleSetCurrentUser(state, action as setCurrentUserAction); - case FETCH_PROFILE_SUCCESS: - return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction); - case UPDATE_PROFILE_SUCCESS: - return handleFetchProfileSuccess(state, action as updateProfileSuccessAction); - - } - return state; -} - -function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction): AuthState { - return { - ...state, - isAuthenticated: true, - currentUser: { - id: '', - email - } - }; -}; - -function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState { - return { - ...state, - isAuthenticated: true, - currentUser: { - ...profile, - } - }; -}; - -function handleUpdateProfileSuccess(state: AuthState, { profile }: updateProfileSuccessAction): AuthState { - return { - ...state, - isAuthenticated: true, - currentUser: { - ...profile, - } - }; -}; \ No newline at end of file diff --git a/client/src/store/reducers/flags.ts b/client/src/store/reducers/flags.ts deleted file mode 100644 index f42701b..0000000 --- a/client/src/store/reducers/flags.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Action } from "redux"; - -export interface FlagsState { - actions: { [actionName: string]: ActionState } -} - -export interface ActionState { - isLoading: boolean -} - -const defaultState = { - actions: {} -}; - -export function flagsReducer(state = defaultState, action: Action): FlagsState { - const matches = (/^(.*)_((SUCCESS)|(FAILURE)|(REQUEST))$/).exec(action.type); - - if(!matches) return state; - - const actionPrefix = matches[1]; - - return { - ...state, - actions: { - ...state.actions, - [actionPrefix]: { - isLoading: matches[2] === 'REQUEST' - } - } - }; - -} \ No newline at end of file diff --git a/client/src/store/reducers/root.ts b/client/src/store/reducers/root.ts deleted file mode 100644 index 337b8b0..0000000 --- a/client/src/store/reducers/root.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { combineReducers } from 'redux'; -import { flagsReducer, FlagsState } from './flags'; -import { authReducer, AuthState } from './auth'; -import { workgroupsReducer, WorkgroupsState } from './workgroups'; - -export interface RootState { - auth: AuthState, - flags: FlagsState, - workgroups: WorkgroupsState, -} - -export const rootReducer = combineReducers({ - flags: flagsReducer, - auth: authReducer, - workgroups: workgroupsReducer, -}); \ No newline at end of file diff --git a/client/src/store/reducers/workgroups.ts b/client/src/store/reducers/workgroups.ts deleted file mode 100644 index c692167..0000000 --- a/client/src/store/reducers/workgroups.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Action } from "redux"; -import { User } from "../../types/user"; -import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth"; -import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction, updateProfileSuccessAction, UPDATE_PROFILE_SUCCESS, updateProfileRequestAction } from "../actions/profile"; -import { Workgroup } from "../../types/workgroup"; -import { FETCH_WORKGROUPS_SUCCESS, fetchWorkgroupsSuccessAction } from "../actions/workgroups"; - -export interface WorkgroupsState { - workgroupsById: { [id in string]: Workgroup } -} - -const defaultState = { - workgroupsById: {} -}; - -export function workgroupsReducer(state = defaultState, action: Action): WorkgroupsState { - switch (action.type) { - case FETCH_WORKGROUPS_SUCCESS: - return handleFetchWorkgroups(state, action as fetchWorkgroupsSuccessAction); - - } - return state; -} - -function handleFetchWorkgroups(state: WorkgroupsState, { workgroups }: fetchWorkgroupsSuccessAction): WorkgroupsState { - const workgroupsById = { - ...state.workgroupsById, - }; - - workgroups.forEach(wg => { - workgroupsById[wg.id] = wg; - }); - - return { - ...state, - workgroupsById, - }; -}; \ No newline at end of file diff --git a/client/src/store/sagas/failure.ts b/client/src/store/sagas/failure.ts deleted file mode 100644 index de90822..0000000 --- a/client/src/store/sagas/failure.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { UnauthorizedError } from "../../util/daddy"; -import { all, takeEvery } from 'redux-saga/effects'; - -export function* failureRootSaga() { - yield all([ - takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga), - ]); -} - -export function* failuresSaga(action) { - if (action.error instanceof UnauthorizedError) { - // TODO Implements better authorization error handling - window.location.reload(); - } -} - -export function patternFromRegExp(re: any) { - return (action: any) => { - return re.test(action.type); - }; -} \ No newline at end of file diff --git a/client/src/store/sagas/init.ts b/client/src/store/sagas/init.ts deleted file mode 100644 index b809870..0000000 --- a/client/src/store/sagas/init.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { all, put } from "redux-saga/effects"; -import { fetchProfile } from "../actions/profile"; - -export function* initRootSaga() { - yield all([ - fetchUserProfileSaga(), - ]); -} - -export function* fetchUserProfileSaga() { - yield put(fetchProfile()); -} \ No newline at end of file diff --git a/client/src/store/sagas/profile.ts b/client/src/store/sagas/profile.ts deleted file mode 100644 index 9e0864a..0000000 --- a/client/src/store/sagas/profile.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { DaddyClient, getClient } from "../../util/daddy"; -import { Config } from "../../config"; -import { all, takeLatest, put, select } from "redux-saga/effects"; -import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS, updateProfileRequestAction, UPDATE_PROFILE_REQUEST, UPDATE_PROFILE_FAILURE, UPDATE_PROFILE_SUCCESS } from "../actions/profile"; -import { SET_CURRENT_USER } from "../actions/auth"; -import { User } from "../../types/user"; - -export function* profileRootSaga() { - yield all([ - takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga), - takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga), - takeLatest(UPDATE_PROFILE_REQUEST, updateProfileSaga), - ]); -} - -export function* onCurrentUserChangeSaga() { - yield put(fetchProfile()); -} - -export function* fetchProfileSaga() { - const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint); - - let profile: User; - try { - profile = yield client.fetchProfile().then(result => result.userProfile); - } catch(err) { - yield put({ type: FETCH_PROFILE_FAILURE, err }); - return; - } - - yield put({type: FETCH_PROFILE_SUCCESS, profile }); -} - -export function* updateProfileSaga({ changes }: updateProfileRequestAction) { - const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint); - - let profile: User; - try { - profile = yield client.updateProfile(changes).then(result => result.updateProfile); - } catch(err) { - yield put({ type: UPDATE_PROFILE_FAILURE, err }); - return; - } - - yield put({type: UPDATE_PROFILE_SUCCESS, profile }); -} \ No newline at end of file diff --git a/client/src/store/sagas/root.ts b/client/src/store/sagas/root.ts deleted file mode 100644 index b09eca5..0000000 --- a/client/src/store/sagas/root.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { all } from 'redux-saga/effects'; -import { failureRootSaga } from './failure'; -import { initRootSaga } from './init'; -import { profileRootSaga } from './profile'; -import { workgroupsRootSaga } from './workgroups'; - -export function* rootSaga() { - yield all([ - initRootSaga(), - failureRootSaga(), - profileRootSaga(), - workgroupsRootSaga(), - ]); -} diff --git a/client/src/store/sagas/workgroups.ts b/client/src/store/sagas/workgroups.ts deleted file mode 100644 index a6d9cc3..0000000 --- a/client/src/store/sagas/workgroups.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getClient } from "../../util/daddy"; -import { Config } from "../../config"; -import { all, takeLatest, put } from "redux-saga/effects"; -import { FETCH_WORKGROUPS_SUCCESS, FETCH_WORKGROUPS_FAILURE, FETCH_WORKGROUPS_REQUEST } from "../actions/workgroups"; -import { Workgroup } from "../../types/workgroup"; - -export function* workgroupsRootSaga() { - yield all([ - takeLatest(FETCH_WORKGROUPS_REQUEST, fetchWorkgroupsSaga), - ]); -} - -export function* fetchWorkgroupsSaga() { - const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint); - - let workgroups: [Workgroup]; - try { - workgroups = yield client.fetchWorkgroups().then(result => result.workgroups); - } catch(err) { - yield put({ type: FETCH_WORKGROUPS_FAILURE, err }); - return; - } - - yield put({type: FETCH_WORKGROUPS_SUCCESS, workgroups }); -} \ No newline at end of file diff --git a/client/src/store/selectors/flags.ts b/client/src/store/selectors/flags.ts deleted file mode 100644 index 1f51cb4..0000000 --- a/client/src/store/selectors/flags.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function selectFlagsIsLoading(state: any, ...actionPrefixes: any[]) { - const { actions } = state.flags; - return actionPrefixes.reduce((isLoading, prefix) => { - if (!(prefix in actions)) return isLoading; - return isLoading || actions[prefix].isLoading; - }, false); -}; \ No newline at end of file diff --git a/client/src/store/store.ts b/client/src/store/store.ts deleted file mode 100644 index 911fd81..0000000 --- a/client/src/store/store.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createStore, applyMiddleware, compose } from 'redux' -import createSagaMiddleware from 'redux-saga' -import { rootReducer } from './reducers/root' -import { rootSaga } from './sagas/root' - -let reduxMiddlewares = []; - -if (process.env.NODE_ENV !== 'production') { - const createLogger = require('redux-logger').createLogger; - const loggerMiddleware = createLogger({ - collapsed: true, - diff: true - }); - reduxMiddlewares.push(loggerMiddleware); -} - -const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - -// create the saga middleware -const sagaMiddleware = createSagaMiddleware() -reduxMiddlewares.push(sagaMiddleware); - -// mount it on the Store -export const store = createStore( - rootReducer, - composeEnhancers(applyMiddleware(...reduxMiddlewares)), -) - -// then run the saga -sagaMiddleware.run(rootSaga); \ No newline at end of file diff --git a/client/src/util/daddy.ts b/client/src/util/daddy.ts deleted file mode 100644 index aa48178..0000000 --- a/client/src/util/daddy.ts +++ /dev/null @@ -1,134 +0,0 @@ -import ApolloClient, { DefaultOptions } from 'apollo-client'; -import { InMemoryCache } from 'apollo-cache-inmemory'; -import { split } from 'apollo-link'; -import { HttpLink } from 'apollo-link-http'; -import { WebSocketLink } from 'apollo-link-ws'; -import { getMainDefinition, variablesInOperation } from 'apollo-utilities'; -import gql from 'graphql-tag'; -import { ProfileChanges } from '../store/actions/profile'; - -export class UnauthorizedError extends Error { - constructor(...args: any[]) { - super(...args) - Object.setPrototypeOf(this, UnauthorizedError.prototype); - } -} - -let client: DaddyClient - -export function getClient(graphQLEndpoint: string, subscriptionEndpoint: string): DaddyClient { - if (!client) { - client = new DaddyClient(graphQLEndpoint, subscriptionEndpoint); - } - - return client; -} - -export class DaddyClient { - - gql: ApolloClient - - constructor(graphQLEndpoint: string, subscriptionEndpoint: string) { - const wsLink = new WebSocketLink({ - uri: subscriptionEndpoint, - options: { - reconnect: true - } - }); - - const httpLink = new HttpLink({ - uri: graphQLEndpoint, - fetchOptions: { - mode: 'cors', - credentials: 'include', - } - }); - - const link = split( - ({ query }) => { - const definition = getMainDefinition(query); - return ( - definition.kind === 'OperationDefinition' && - definition.operation === 'subscription' - ); - }, - wsLink, - httpLink, - ); - - const defaultOptions: DefaultOptions = { - watchQuery: { - fetchPolicy: 'no-cache', - errorPolicy: 'ignore', - }, - query: { - fetchPolicy: 'no-cache', - errorPolicy: 'all', - }, - }; - - this.gql = new ApolloClient({ - link: link, - cache: new InMemoryCache(), - defaultOptions, - }); - } - - fetchProfile() { - return this.gql.query({ - query: gql` - query { - userProfile { - id, - name, - email, - createdAt, - connectedAt - } - }` - }) - .then(this.assertAuthorization) - } - - fetchWorkgroups() { - return this.gql.query({ - query: gql` - query { - workgroups { - id, - name, - createdAt, - closedAt, - members { - id - } - } - }` - }) - .then(this.assertAuthorization) - } - - updateProfile(changes: ProfileChanges) { - return this.gql.mutate({ - variables: { - changes, - }, - mutation: gql` - mutation updateProfile($changes: ProfileChanges!) { - updateProfile(changes: $changes) { - name, - email, - createdAt, - connectedAt - } - }`, - }) - .then(this.assertAuthorization) - } - - assertAuthorization({ status, data }: any) { - if (status === 401) return Promise.reject(new UnauthorizedError()); - return data; - } - -} \ No newline at end of file diff --git a/internal/route/mount.go b/internal/route/mount.go index 973424d..af4d08b 100644 --- a/internal/route/mount.go +++ b/internal/route/mount.go @@ -33,7 +33,6 @@ func Mount(r *chi.Mux, config *config.Config) error { AllowCredentials: config.HTTP.CORS.AllowCredentials, Debug: config.Debug, }).Handler) - r.Use(oidc.Middleware) r.Use(session.UserEmailMiddleware) gql := handler.New( diff --git a/internal/session/user_email.go b/internal/session/user_email.go index b1dc0c3..171f23e 100644 --- a/internal/session/user_email.go +++ b/internal/session/user_email.go @@ -21,7 +21,9 @@ func UserEmailMiddleware(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { userEmail, err := GetUserEmail(w, r) if err != nil { - panic(errors.Wrap(err, "could not find user email")) + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + + return } ctx := WithUserEmail(r.Context(), userEmail)