diff --git a/package-lock.json b/package-lock.json index dc039670..f7576edd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hhd-ui", - "version": "0.18.0", + "version": "0.18.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "hhd-ui", - "version": "0.18.0", + "version": "0.18.2", "dependencies": { "@ag-grid-community/client-side-row-model": "^30.0.3", "@ag-grid-community/core": "^30.0.3", @@ -29,7 +29,7 @@ "date-fns": "^2.30.0", "graceful-fs": "^4.2.11", "internal-nav-helper": "^3.1.0", - "keycloak-js": "^25.0.2", + "keycloak-js": "^25.0.5", "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", "luxon": "^3.3.0", @@ -948,22 +948,6 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.12.tgz", - "integrity": "sha512-ohqLPc7i67yunArPj1+/FeeJ7AgwAjHqKZ512ADk3WsE3FHU9l+m5aa7NdxXr0HmN1bjDlUslBjWNbFlD9y12Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -5590,10 +5574,9 @@ "license": "ISC" }, "node_modules/keycloak-js": { - "version": "25.0.2", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.2.tgz", - "integrity": "sha512-ACLf5O5PqzfDJwGqvLpqM0kflYWmyl3+T7M2C23gztJYccDxdfNP54+B9OkXz2GnDpLUId0ceoA+lbHw9t4Wng==", - "license": "Apache-2.0", + "version": "25.0.6", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.6.tgz", + "integrity": "sha512-Km+dc+XfNvY6a4az5jcxTK0zPk52ns9mAxLrHj7lF3V+riVYvQujfHmhayltJDjEpSOJ4C8a57LFNNKnNnRP2g==", "dependencies": { "js-sha256": "^0.11.0", "jwt-decode": "^4.0.0" @@ -9760,13 +9743,6 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" }, - "@esbuild/darwin-x64": { - "version": "0.18.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.12.tgz", - "integrity": "sha512-ohqLPc7i67yunArPj1+/FeeJ7AgwAjHqKZ512ADk3WsE3FHU9l+m5aa7NdxXr0HmN1bjDlUslBjWNbFlD9y12Q==", - "dev": true, - "optional": true - }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -12860,9 +12836,9 @@ "version": "3.0.0" }, "keycloak-js": { - "version": "25.0.2", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.2.tgz", - "integrity": "sha512-ACLf5O5PqzfDJwGqvLpqM0kflYWmyl3+T7M2C23gztJYccDxdfNP54+B9OkXz2GnDpLUId0ceoA+lbHw9t4Wng==", + "version": "25.0.6", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.6.tgz", + "integrity": "sha512-Km+dc+XfNvY6a4az5jcxTK0zPk52ns9mAxLrHj7lF3V+riVYvQujfHmhayltJDjEpSOJ4C8a57LFNNKnNnRP2g==", "requires": { "js-sha256": "^0.11.0", "jwt-decode": "^4.0.0" diff --git a/package.json b/package.json index 83fce011..6377c9a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hhd-ui", - "version": "0.18.1", + "version": "0.18.2", "private": true, "dependencies": { "@ag-grid-community/client-side-row-model": "^30.0.3", @@ -24,7 +24,7 @@ "date-fns": "^2.30.0", "graceful-fs": "^4.2.11", "internal-nav-helper": "^3.1.0", - "keycloak-js": "^25.0.2", + "keycloak-js": "^25.0.5", "lodash.debounce": "^4.0.8", "lodash.isequal": "^4.5.0", "luxon": "^3.3.0", diff --git a/src/app-bundles/create-rest-bundle.js b/src/app-bundles/create-rest-bundle.js index 785023a9..08b3ce79 100644 --- a/src/app-bundles/create-rest-bundle.js +++ b/src/app-bundles/create-rest-bundle.js @@ -274,7 +274,7 @@ const createRestBundle = (opts) => { dispatch({ type: actions.ERROR, payload: { - _err: { err: err }, + _err: { err: err, response: body }, _isLoading: false, _isSaving: false, _fetchCount: ++fetchCount, @@ -348,7 +348,7 @@ const createRestBundle = (opts) => { dispatch({ type: actions.ERROR, payload: { - _err: { err: err }, + _err: { err: err, response: body }, _isSaving: false, }, }); @@ -392,12 +392,12 @@ const createRestBundle = (opts) => { }); // save changes to the server - apiPut(url, item, (err, _body) => { + apiPut(url, item, (err, body) => { if (err) { dispatch({ type: actions.ERROR, payload: { - _err: { err: err }, + _err: { err: err, response: body }, _isSaving: false, }, }); @@ -446,12 +446,12 @@ const createRestBundle = (opts) => { }); // update the state on the server now - apiDelete(url, (err, _body) => { + apiDelete(url, (err, body) => { if (err) { dispatch({ type: actions.ERROR, payload: { - _err: { err: err }, + _err: { err: err, response: body }, _isSaving: false, }, }); diff --git a/src/app-bundles/index.js b/src/app-bundles/index.js index 306d1e40..151d11ba 100644 --- a/src/app-bundles/index.js +++ b/src/app-bundles/index.js @@ -48,7 +48,6 @@ import notificationBundle from './notification-bundle'; import printBundle from './print-bundle'; import profileAlertsBundle from './profile-alerts-bundle'; import profileAlertSubscriptionsBundle from './profile-alert-subscriptions-bundle'; -import profileBundle from './profile-bundle'; import projectAlertConfigs from './project-alert-configs'; import projectMembersBundle from './project-members-bundle'; import projectionBundle from './projection-bundle'; @@ -141,7 +140,6 @@ export default composeBundles( printBundle, profileAlertsBundle, profileAlertSubscriptionsBundle, - profileBundle, projectAlertConfigs, projectMembersBundle, projectionBundle, diff --git a/src/app-bundles/profile-bundle.js b/src/app-bundles/profile-bundle.js deleted file mode 100644 index 1415cf63..00000000 --- a/src/app-bundles/profile-bundle.js +++ /dev/null @@ -1,88 +0,0 @@ -import { isLoggedIn } from '../userService.ts'; -import createRestBundle from './create-rest-bundle'; -import { createSelector } from 'redux-bundler'; - -export default createRestBundle({ - name: 'profile', - uid: 'id', - initialFetch: true, - staleAfter: 0, - persist: false, - routeParam: 'id', - getTemplate: '/my_profile', - putTemplate: '/profiles/{:item.slug}', - postTemplate: '/profiles', - deleteTemplate: '', - fetchActions: ['URL_UPDATED'], - forceFetchActions: ['PROFILE_SAVE_FINISHED', '@@INIT'], - reduceFurther: (state, { type, payload }) => { - switch (type) { - case 'PROFILE_REMOVE_PROFILE': - return Object.assign({}, payload); - default: - return state; - } - }, - addons: { - doRemoveProfile: () => ({ dispatch, store }) => { - dispatch({ - type: 'PROFILE_REMOVE_PROFILE', - payload: store.selectProfileFlags(), - }); - }, - selectProfileRaw: state => state.profile, - selectProfileFlags: createSelector('selectProfileRaw', profile => { - const profileClone = Object.assign({}, profile); - Object.keys(profileClone).forEach((key) => { - if (key[0] !== '_') delete profileClone[key]; - }); - return profileClone; - }), - selectProfileActive: createSelector( - 'selectProfileItems', - (profileItems) => { - if (profileItems.length < 1) return null; - return profileItems[0]; - } - ), - selectProfileId: createSelector('selectProfileActive', (profileActive) => { - if (!profileActive) return null; - return { - profileId: profileActive.id - }; - }), - selectProfileIsAdmin: createSelector('selectProfileActive', (profileActive) => { - if (!profileActive) return null; - return profileActive.is_admin; - }), - selectProfileRoles: createSelector('selectProfileActive', (profileActive) => { - if (!profileActive) return null; - return profileActive.roles; - }), - selectProfileRolesObject: createSelector('selectProfileRoles', (profileRoles) => { - if (!profileRoles) return null; - return profileRoles.reduce((accum, elem) => { - const groupRole = elem.split('.'); - const group = groupRole[0]; - const role = groupRole[1]; - - return { - ...accum, - [group]: (accum[group] || []).concat([role]), - }; - }, {}); - }), - reactProfileExists: createSelector( - 'selectProfileFlags', - 'selectProfileActive', - (flags, profile) => { - if (isLoggedIn() && !flags._isLoading && !profile) { - return { - actionCreator: 'doProfileSave', - args: [{}], - }; - } - } - ), - }, -}); diff --git a/src/app-bundles/project-members-bundle.js b/src/app-bundles/project-members-bundle.js index 4bb52a07..7c1dfb98 100644 --- a/src/app-bundles/project-members-bundle.js +++ b/src/app-bundles/project-members-bundle.js @@ -8,7 +8,7 @@ export default createRestBundle({ staleAfter: 0, persist: false, getTemplate: '/projects/:projectId/members', - urlParamSelectors: ['selectProjectsIdByRoute', 'selectProfileId'], + urlParamSelectors: ['selectProjectsIdByRoute'], fetchActions: ['URL_UPDATED', 'PROJECTS_FETCH_FINISHED'], forceFetchActions: [ 'PROJECTMEMBERS_ADD_USER_FINISHED', diff --git a/src/app-components/pageContent.jsx b/src/app-components/pageContent.jsx index 04bf6676..163b2959 100644 --- a/src/app-components/pageContent.jsx +++ b/src/app-components/pageContent.jsx @@ -2,29 +2,9 @@ import React from 'react'; import { connect } from 'redux-bundler-react'; import { classArray } from '../common/helpers/utils'; +import { useGetMyProfile } from '../app-services/collections/profiles.ts'; const hasDevBanner = import.meta.env.VITE_DEVELOPMENT_BANNER === 'true'; -// const blacklist = ['/', '/help']; - -// const PageContent = connect( -// 'selectRelativePathname', -// ({ -// relativePathname: pathname, -// children, -// }) => { -// const pageClasses = classArray([ -// 'page-margin', -// hasDevBanner && 'banner', -// ]); - -// return ( -//
-// {children} -//
-// ); -// } -// ); - const blacklist = ['/', '/help']; const PageContent = connect( @@ -33,6 +13,8 @@ const PageContent = connect( relativePathname: pathname, children, }) => { + const { data: profile } = useGetMyProfile({ showToast: true }); + const pageClasses = classArray([ blacklist.includes(pathname) ? '' : 'page-margin', hasDevBanner && 'banner', @@ -43,7 +25,7 @@ const PageContent = connect( {children} ); - } + }, ); export default PageContent; diff --git a/src/app-components/role-filter.jsx b/src/app-components/role-filter.jsx index 707d7c4f..33320088 100644 --- a/src/app-components/role-filter.jsx +++ b/src/app-components/role-filter.jsx @@ -1,5 +1,6 @@ import React from 'react'; -import { connect } from 'redux-bundler-react'; + +import { extractRolesObject, useGetMyProfile } from '../app-services/collections/profiles.ts'; export const isUserAllowed = (profileRoles, isAdmin, allowRoles = [], denyRoles = []) => { // If there are no profile roles, user shouldn't be here @@ -40,24 +41,22 @@ export const isUserAllowed = (profileRoles, isAdmin, allowRoles = [], denyRoles return showChildren; }; -export default connect( - 'selectProfileRolesObject', - 'selectProfileIsAdmin', - ({ - profileRolesObject, - profileIsAdmin, - allowRoles = [], - denyRoles = [], - alt = null, - children, - }) => { - const showChildren = isUserAllowed(profileRolesObject, profileIsAdmin, allowRoles, denyRoles); - - if (showChildren) { - return <>{children}; - } else { - const Alt = alt ? alt : null; - return Alt ? : null; - } +export default ({ + allowRoles = [], + denyRoles = [], + alt = null, + children, +}) => { + const { data: profile } = useGetMyProfile({}); + const { is_admin } = profile || {}; + const roles = extractRolesObject(profile); + + const showChildren = isUserAllowed(roles, is_admin, allowRoles, denyRoles); + + if (showChildren) { + return <>{children}; + } else { + const Alt = alt ? alt : null; + return Alt ? : null; } -); +}; diff --git a/src/app-pages/instrument/notes.jsx b/src/app-pages/instrument/notes.jsx index 1c82dc41..fbb9a933 100644 --- a/src/app-pages/instrument/notes.jsx +++ b/src/app-pages/instrument/notes.jsx @@ -5,6 +5,7 @@ import { Edit } from '@mui/icons-material'; import Button from '../../app-components/button'; import Card from '../../app-components/card'; import RoleFilter from '../../app-components/role-filter'; +import { useGetMyProfile } from '../../app-services/collections/profiles.ts'; const NoteEditor = connect( 'selectInstrumentsByRoute', @@ -105,22 +106,21 @@ const NoteItem = ({ note, editable, save }) => { }; export default connect( - 'selectProfileActive', 'selectProjectsByRoute', 'selectInstrumentNotesItems', 'doInstrumentNotesSave', 'doInstrumentNotesDelete', ({ - profileActive, projectsByRoute: project, instrumentNotesItems: notes, doInstrumentNotesSave: save, doInstrumentNotesDelete: del, }) => { const [isAdding, setIsAdding] = useState(false); + const { data: profile } = useGetMyProfile({}); + const { id } = profile || {}; const sorted = notes.sort(); - const { id } = profileActive || {}; return ( project && ( diff --git a/src/app-pages/profile/userProfile.jsx b/src/app-pages/profile/userProfile.jsx index da9c5637..211a694c 100644 --- a/src/app-pages/profile/userProfile.jsx +++ b/src/app-pages/profile/userProfile.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { connect } from 'redux-bundler-react'; import { formatDistance } from 'date-fns'; +import { Engineering } from '@mui/icons-material'; import Card from '../../app-components/card'; import TabContainer from '../../app-components/tab'; @@ -10,12 +11,15 @@ import './userProfile.css'; const urlify = str => str?.toLowerCase().split(' ').join('-'); -const buildProjectContent = (projects = []) => { - if (!projects.length) return

No Projects!

; - +const buildProjectContent = (_projects = []) => { return ( -
- Favorited Project List goes here! +
+ +
Currently Under Construction
); }; diff --git a/src/app-pages/project/admin/index.jsx b/src/app-pages/project/admin/index.jsx index 0c5062b5..59e5011d 100644 --- a/src/app-pages/project/admin/index.jsx +++ b/src/app-pages/project/admin/index.jsx @@ -15,7 +15,6 @@ const AdminPage = connect( 'doModalOpen', 'doUsersDeleteUser', 'selectDomainsItemsByGroup', - 'selectProfileActive', 'selectMembersObject', ({ doModalOpen, diff --git a/src/app-pages/project/dashboard/index.jsx b/src/app-pages/project/dashboard/index.jsx index 379150ce..cb72c3a9 100644 --- a/src/app-pages/project/dashboard/index.jsx +++ b/src/app-pages/project/dashboard/index.jsx @@ -9,20 +9,20 @@ import InstrumentStatusCard from './cards/instrumentStatusCard'; import InstrumentTypeCard from './cards/instrumentTypeCard'; import { isUserAllowed } from '../../../app-components/role-filter'; import ReportConfigsCard from './cards/reportConfigsCard'; +import { extractRolesObject, useGetMyProfile } from '../../../app-services/collections/profiles.ts'; const ProjectDashboard = connect( 'doFetchDataLoggersByProjectId', 'doFetchReportConfigurationsByProjectId', - 'selectProfileRolesObject', - 'selectProfileIsAdmin', 'selectProjectsByRoute', ({ doFetchDataLoggersByProjectId, doFetchReportConfigurationsByProjectId, - profileRolesObject, - profileIsAdmin, projectsByRoute: project, }) => { + const { data: profile } = useGetMyProfile({}); + const { is_admin } = profile || {}; + const roles = extractRolesObject(profile); const { slug } = project; useEffect(() => { @@ -36,7 +36,7 @@ const ProjectDashboard = connect(
- {isUserAllowed(profileRolesObject, profileIsAdmin, [`${slug.toUpperCase()}.*`]) + {isUserAllowed(roles, is_admin, [`${slug.toUpperCase()}.*`]) ? ( <> @@ -48,7 +48,7 @@ const ProjectDashboard = connect(
- {isUserAllowed(profileRolesObject, profileIsAdmin, [`${slug.toUpperCase()}.*`]) + {isUserAllowed(roles, is_admin, [`${slug.toUpperCase()}.*`]) ? : null } diff --git a/src/app-pages/project/index.jsx b/src/app-pages/project/index.jsx index c58133f2..17533aea 100644 --- a/src/app-pages/project/index.jsx +++ b/src/app-pages/project/index.jsx @@ -21,20 +21,21 @@ import SecondaryNavBar from '../../app-components/navigation/secondaryNavBar'; import QaQc from './qa-qc'; import Uploader from './uploader'; import { isUserAllowed } from '../../app-components/role-filter'; +import { extractRolesObject, useGetMyProfile } from '../../app-services/collections/profiles.ts'; const Title = ({ text, icon }) => ( {icon} {text} ); const Project = connect( - 'selectProfileRolesObject', - 'selectProfileIsAdmin', 'selectProjectsByRoute', ({ - profileRolesObject, - profileIsAdmin, projectsByRoute: project, }) => { + const { data: profile } = useGetMyProfile({}); + const { is_admin } = profile || {}; + const roles = extractRolesObject(profile); + if (!project) { return

Loading Project Details...

; } @@ -58,7 +59,7 @@ const Project = connect( uri: '#all-instruments', }], // Active if User === Project Member - ...isUserAllowed(profileRolesObject, profileIsAdmin, [`${project.slug.toUpperCase()}.*`]) ? [{ + ...isUserAllowed(roles, is_admin, [`${project.slug.toUpperCase()}.*`]) ? [{ title: } />, content: <Uploader />, paddingSmall: true, @@ -75,7 +76,7 @@ const Project = connect( uri: '#qa-qc', }] : [], // Active if User === Project Admin - ...isUserAllowed(profileRolesObject, profileIsAdmin, [`${project.slug.toUpperCase()}.ADMIN`]) ? [{ + ...isUserAllowed(roles, is_admin, [`${project.slug.toUpperCase()}.ADMIN`]) ? [{ title: <Title text='Data Loggers' icon={<TimelineOutlined fontSize='inherit' sx={{ marginBottom: '2px' }} />} />, content: <DataLoggers />, paddingSmall: true, diff --git a/src/app-services/collections/profiles.ts b/src/app-services/collections/profiles.ts new file mode 100644 index 00000000..8005c4c4 --- /dev/null +++ b/src/app-services/collections/profiles.ts @@ -0,0 +1,97 @@ +import { useQuery } from '@tanstack/react-query'; +import { toast } from 'react-toastify'; + +import { apiGet } from '../fetch-helpers'; +import { getToken } from '../../userService'; + +type Profile = { + id: string, + tokens: string[], + is_admin: boolean, + roles: string[], + username: string, + display_name: string, + email: string, +} + +const handleResponse = (data: Promise<JSON>) => { + return data.then( + (value) => Promise.resolve(value), + (reason) => { + console.log('test reason:', reason); + toast.error( + `test error: ${reason}`, + { + autoClose: 7500, + closeOnClick: true, + draggable: true, + } + ); + return Promise.reject(reason); + }, + ); +}; + +export const useGetMyProfile = ({ showToast }: { showToast?: boolean}, opts: ClientQueryOptions) => { + const uri = `/my_profile`; + const token = getToken(); + + return useQuery({ + queryKey: [`my_profile`, token], + queryFn: () => { + const data = apiGet(uri); + + if (showToast) { + return handleResponse(data); + } else { + return data; + } + }, + // staleTime: (60*1000), + ...opts, + }); +}; + +/* -- If a use case exists to manually create a profile, uncomment this: -- */ +// export const useCreateProfile = (toastId: string) => { +// return useMutation({ +// mutationFn: () => { +// const uri = `/profiles`; + +// return apiPost(uri); +// }, +// onError: (err, __, _ctx) => { +// toast.update(toastId, { +// render: 'Failed to create MIDAS profile.', +// type: 'error', +// isLoading: false, +// autoClose: 5000, +// closeOnClick: true, +// draggable: true, +// }); + +// return { error: 'Failed to Create MIDAS Profile', _raw: err }; +// }, +// onSuccess: (body, _, _ctx) => { +// return body; +// }, +// }); +// }; + +/* -- Helper Functions -- */ + +export const extractRolesObject = (profile: Profile) => { + const { roles } = profile || {}; + if (!roles) return null; + + return roles.reduce((accum, elem) => { + const groupRole = elem.split('.'); + const group = groupRole[0]; + const role = groupRole[1]; + + return { + ...accum, + [group]: (accum[group] || []).concat([role]), + }; + }, {} as Record<string, string[]>); +}; diff --git a/src/app-services/fetch-helpers.ts b/src/app-services/fetch-helpers.ts index a634393c..e8862e3e 100644 --- a/src/app-services/fetch-helpers.ts +++ b/src/app-services/fetch-helpers.ts @@ -32,7 +32,10 @@ export const commonFetch = async (root: string, path: string, options: OptsType) const res = await fetch(`${root}${path}`, options); if (res.status === 204) return {} as JSON; - else if (!res.ok) throw new Error(res.statusText); + else if (!res.ok) { + console.log('test res.headers:', res); + throw new Error(res.headers.get('RequestID') || 'An Error Has Occured'); + } return (await res.json()) as JSON; };