diff --git a/horreum-web/package-lock.json b/horreum-web/package-lock.json index 55332859e..1b476a1eb 100644 --- a/horreum-web/package-lock.json +++ b/horreum-web/package-lock.json @@ -18,7 +18,6 @@ "@types/react": "19.2.2", "@types/react-autosuggest": "10.1.11", "@types/react-dom": "19.2.1", - "@types/react-redux": "7.1.20", "@types/uuid": "10.0.0", "@vitejs/plugin-react": "5.0.1", "clsx": "2.1.0", @@ -26,15 +25,15 @@ "github-markdown-css": "5.6.1", "jshint": "2.13.6", "jsonpath": "1.1.1", - "keycloak-js": "23.0.7", "luxon": "3.3.0", "moment": "2.30.1", "monaco-editor": "0.54.0", + "oidc-client-ts": "^3.3.0", "react": "19.2.0", "react-autosuggest": "10.1.0", "react-dom": "19.2.0", "react-markdown": "10.1.0", - "react-redux": "9.2.0", + "react-oidc-context": "^3.3.0", "react-router": "7.9.5", "react-router-dom": "7.9.1", "react-to-print": "3.1.1", @@ -316,15 +315,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1513,15 +1503,6 @@ "@types/unist": "*" } }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", - "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1589,26 +1570,6 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/react-redux": { - "version": "7.1.20", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.20.tgz", - "integrity": "sha512-q42es4c8iIeTgcnB+yJgRTTzftv3eYYvCZOh1Ckn2eX/3o5TdsQYKUWpLoLuGlcY/p+VAhV9IOEZJcWk/vfkXw==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, - "node_modules/@types/react-redux/node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -1930,25 +1891,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3187,14 +3129,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -3396,11 +3330,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/js-sha256": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz", - "integrity": "sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3517,16 +3446,6 @@ "node": ">=18" } }, - "node_modules/keycloak-js": { - "version": "23.0.7", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-23.0.7.tgz", - "integrity": "sha512-OmszsKzBhhm5yP4W1q/tMd+nNnKpOAdeVYcoGhphlv8Fj1bNk4wRTYzp7pn5BkvueLz7fhvKHz7uOc33524YrA==", - "dependencies": { - "base64-js": "^1.5.1", - "js-sha256": "^0.10.1", - "jwt-decode": "^4.0.0" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4320,6 +4239,18 @@ "node": ">=0.10.0" } }, + "node_modules/oidc-client-ts": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.3.0.tgz", + "integrity": "sha512-t13S540ZwFOEZKLYHJwSfITugupW4uYLwuQSSXyKH/wHwZ+7FvgHE7gnNJh1YQIZ1Yd1hKSRjqeXGSUtS0r9JA==", + "license": "Apache-2.0", + "dependencies": { + "jwt-decode": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4658,6 +4589,19 @@ "react": ">=18" } }, + "node_modules/react-oidc-context": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.3.0.tgz", + "integrity": "sha512-302T/ma4AOVAxrHdYctDSKXjCq9KNHT564XEO2yOPxRfxEP58xa4nz+GQinNl8x7CnEXECSM5JEjQJk3Cr5BvA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "oidc-client-ts": "^3.1.0", + "react": ">=16.14.0" + } + }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", diff --git a/horreum-web/package.json b/horreum-web/package.json index 864dd7b12..46679f79b 100644 --- a/horreum-web/package.json +++ b/horreum-web/package.json @@ -14,7 +14,6 @@ "@types/react": "19.2.2", "@types/react-autosuggest": "10.1.11", "@types/react-dom": "19.2.1", - "@types/react-redux": "7.1.20", "@types/uuid": "10.0.0", "@vitejs/plugin-react": "5.0.1", "clsx": "2.1.0", @@ -22,15 +21,15 @@ "github-markdown-css": "5.6.1", "jshint": "2.13.6", "jsonpath": "1.1.1", - "keycloak-js": "23.0.7", "luxon": "3.3.0", "moment": "2.30.1", "monaco-editor": "0.54.0", + "oidc-client-ts": "^3.3.0", "react": "19.2.0", "react-autosuggest": "10.1.0", "react-dom": "19.2.0", "react-markdown": "10.1.0", - "react-redux": "9.2.0", + "react-oidc-context": "^3.3.0", "react-router": "7.9.5", "react-router-dom": "7.9.1", "react-to-print": "3.1.1", diff --git a/horreum-web/src/App.tsx b/horreum-web/src/App.tsx index 90edafd15..320621e63 100644 --- a/horreum-web/src/App.tsx +++ b/horreum-web/src/App.tsx @@ -1,16 +1,16 @@ -import React, {useState} from "react" +import React, {useContext, useEffect, useState} from "react" import "@patternfly/patternfly/patternfly.css" //have to use this import to customize scss-variables.scss import {BarsIcon} from '@patternfly/react-icons'; import { - Brand, + Brand, Bullseye, Button, Masthead, MastheadBrand, MastheadContent, MastheadLogo, MastheadMain, MastheadToggle, Nav, NavItem, NavList, - Page, PageSidebar, PageSidebarBody, SkipToContent, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem + Page, PageSidebar, PageSidebarBody, SkipToContent, Spinner, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; import { @@ -21,13 +21,9 @@ import { RouterProvider, } from "react-router-dom" -import {Provider, useSelector} from "react-redux" -import {isAdminSelector, isManagerSelector, LoginLogout} from "./auth" -import {initKeycloak} from "./keycloak" +import {LoginLogout} from "./auth/auth" import {UserProfileLink, UserSettings} from "./domain/user/UserSettings" -import store from "./store" - import Run from "./domain/runs/Run" import AllTests from "./domain/tests/AllTests" import Test from "./domain/tests/Test" @@ -43,9 +39,17 @@ import Banner from "./Banner" import NotFound from "./404" import About from "./About" -import ContextProvider from "./context/appContext"; +import AppContextProvider from "./context/AppContext"; import TableReportConfigPage from "./domain/reports/TableReportConfigPage"; import TableReportPage from "./domain/reports/TableReportPage"; +import {createUserManager} from "./auth/oidc"; +import {AuthProvider} from "react-oidc-context"; +import {KeycloakConfig} from "./generated"; +import {configApi} from "./api"; +import AuthBridgeContextProvider, {AuthBridgeContext} from "./context/AuthBridgeContext"; +import {AuthContextType} from "./context/@types/authContextTypes"; +import {User} from "oidc-client-ts"; +import CallbackSSO from "./auth/CallbackSSO"; const router = createBrowserRouter( createRoutesFromElements( @@ -64,6 +68,9 @@ const router = createBrowserRouter( }/> }/> + + {/* matches the OIDC redirect_uri configured in oidc.ts */} + }/> ), { future: { @@ -84,58 +91,54 @@ const router = createBrowserRouter( ); export default function App() { - initKeycloak(store.getState()) + const [horreumOidcConfig, setHorreumOidcConfig] = useState() + + const onSignInCallback = (user: User | undefined) => { + // TODO: find a way to get rid of the CallbackSSO route and use this callback to redirect to the last visited page + const redirectUrl = (user?.state as { history?: string }).history; + window.history.replaceState({}, document.title, redirectUrl); + }; + + const onSignOutCallback = () => { + window.history.replaceState({}, document.title, window.location.pathname); + }; + + useEffect(() => { + configApi.keycloak().then(setHorreumOidcConfig) + }, []); + + if (!horreumOidcConfig) { + return ( + + + + ) + } + + // if using oidc let's wrap the entire app with AuthProvider + const userManager = createUserManager(horreumOidcConfig) return ( - - - - - + + {/* if url is empty or null -> use basic authentication */} + + + + + + ) -} +} function Main() { - const isAdmin = useSelector(isAdminSelector) - const isManager = useSelector(isManagerSelector) - /* - const isManager = useSelector(isManagerSelector) - const isAuthenticated = useSelector(isAuthenticatedSelector) - const [rolesFilter, setRolesFilter] = useState(ONLY_MY_OWN) - - const [activeGroup, setActiveGroup] = useState(''); - const [activeItem, setActiveItem] = useState('ungrouped_item-1'); - - const onSelect = (result: { itemId: number | string; groupId: number | string | null }) => { - setActiveGroup(result.groupId as string); - setActiveItem(result.itemId as string); - }; - - const profile = useSelector(userProfileSelector) - // const watchingTests = useSelector(selectors.watching) - const navWatching: any[] = [] - */ - /* - watchingTests.forEach((watching, id) => { - const test = selectors.get(id) - watching?.forEach((value, index) => { - if ( value === profile?.username) { - navWatching.push( - - {test} - - ) - } - }) + const { isManager, isAdmin } = useContext(AuthBridgeContext) as AuthContextType; - }) - */ + const [sidebarOpen, setSidebarOpen] = useState(window.sessionStorage.getItem("sidebarOpen")?.toLowerCase() === "true"); - const [sidebarOpen, setSidebarOpen] = useState(true); + useEffect(() => { + window.sessionStorage.setItem("sidebarOpen", sidebarOpen.toString()) + }, [sidebarOpen]); const headerToolbar = ( @@ -144,11 +147,6 @@ function Main() { - {/* { isAdmin && ( - - - - )} */} @@ -206,7 +204,7 @@ function Main() { Schemas - {(isAdmin || isManager ) && ( + {(isAdmin() || isManager() ) && ( Administration diff --git a/horreum-web/src/Login.tsx b/horreum-web/src/Login.tsx index bd1b0eedb..dd5fb077b 100644 --- a/horreum-web/src/Login.tsx +++ b/horreum-web/src/Login.tsx @@ -1,11 +1,9 @@ import {useContext, useEffect, useState} from "react"; -import {AppContext} from "./context/appContext"; -import {AppContextType} from "./context/@types/appContextTypes"; +import {AppContext} from "./context/AppContext"; import {Button, Form, FormGroup, Spinner, TextInput} from '@patternfly/react-core'; import {Modal} from '@patternfly/react-core/deprecated'; -import {userApi} from "./api"; -import store from "./store"; -import {BASIC_AUTH, UPDATE_ROLES, AFTER_LOGOUT, STORE_PROFILE, UPDATE_DEFAULT_TEAM} from "./auth"; +import {AuthBridgeContext} from "./context/AuthBridgeContext"; +import {AuthContextType} from "./context/@types/authContextTypes"; type LoginModalProps = { username: string @@ -17,7 +15,7 @@ type LoginModalProps = { } export default function LoginModal(props: LoginModalProps) { - const {alerting} = useContext(AppContext) as AppContextType; + const { signIn } = useContext(AuthBridgeContext) as AuthContextType; const [username, setUsername] = useState() const [password, setPassword] = useState() const [creating, setCreating] = useState(false) @@ -26,6 +24,7 @@ export default function LoginModal(props: LoginModalProps) { setUsername(undefined) setPassword(undefined) }, [props.isOpen]) + return { setCreating(true) - store.dispatch({type: BASIC_AUTH, username, password}); - - userApi.getRoles() - .then(roles => { - alerting.dispatchInfo("LOGIN", "Log in successful", "Successful log in of user " + username, 3000) - store.dispatch({type: UPDATE_ROLES, authenticated: true, roles: roles}); - userApi.searchUsers(username!).then( - userData => store.dispatch({type: STORE_PROFILE, profile: userData.filter(u => u.username == username).at(0)}), - error => alerting.dispatchInfo("LOGIN", "Unable to get user profile", error, 30000)) - userApi.defaultTeam().then( - response => store.dispatch({ type: UPDATE_DEFAULT_TEAM, team: response }), - error => alerting.dispatchInfo("LOGIN", "Cannot retrieve default team", error, 30000) - ) - }, - error => { - alerting.dispatchInfo("LOGIN", "Failed to authenticate", error, 30000) - store.dispatch({type: AFTER_LOGOUT}); - props.onLoginError?.() - }) - .catch(error => alerting.dispatchInfo("LOGIN", "Could not perform authentication", error, 30000)); - + void signIn(username, password) setCreating(false); props.onClose(); props.onLoginSuccess?.() diff --git a/horreum-web/src/alerts.tsx b/horreum-web/src/alerts.tsx index 578dc32fc..ea2a17376 100644 --- a/horreum-web/src/alerts.tsx +++ b/horreum-web/src/alerts.tsx @@ -1,6 +1,6 @@ import { Alert as PatternflyAlert, AlertActionCloseButton, AlertVariant } from "@patternfly/react-core" -import {AppContext} from "./context/appContext"; +import {AppContext} from "./context/AppContext"; import {useContext} from "react"; import {AppContextType} from "./context/@types/appContextTypes"; diff --git a/horreum-web/src/api.tsx b/horreum-web/src/api.tsx index 7bd90b06e..40fce721c 100644 --- a/horreum-web/src/api.tsx +++ b/horreum-web/src/api.tsx @@ -27,46 +27,27 @@ import { Test, TestListing, TestSummary, Transformer, View, Watch, } from "./generated" -import store from "./store" import {AlertContextType} from "./context/@types/appContextTypes"; +import {AuthBridge} from "./context/AuthBridgeContext"; export * from "./generated/models" const authMiddleware: Middleware = { pre: ctx => { - const keycloak = store.getState().auth.keycloak - const basicAuthToken = store.getState().auth.basicAuthToken - if (keycloak != null && keycloak.authenticated) { - return keycloak.updateToken(30).then( - () => { - if (keycloak != null && keycloak.token != null) { - return { - url: ctx.url, - init: { - ...ctx.init, - headers: {...ctx.init.headers, Authorization: "Bearer " + keycloak.token}, - }, - } - } + if (AuthBridge.isOidc && AuthBridge.accessToken) { + return Promise.resolve({ + url: ctx.url, + init: { + ...ctx.init, + headers: {...ctx.init.headers, Authorization: "Bearer " + AuthBridge.accessToken}, }, - /* e => { - store.dispatch({ - type: ADD_ALERT, - alert: { - type: "TOKEN_UPDATE_FAILED", - title: "Token update failed", - content: , - }, - }) - return Promise.reject(e) - } */ - ) - } else if (basicAuthToken) { + }) + } else if (AuthBridge.accessToken) { return Promise.resolve({ url: ctx.url, init: { ...ctx.init, credentials: "omit", // this prevents the browser from showing the native auth dialog - headers: {...ctx.init.headers, Authorization: "Basic " + basicAuthToken}, + headers: {...ctx.init.headers, Authorization: "Basic " + AuthBridge.accessToken}, }, }) } else { @@ -77,16 +58,6 @@ const authMiddleware: Middleware = { if (ctx.response.ok) { return Promise.resolve(ctx.response) } else if (ctx.response.status === 401 || ctx.response.status === 403) { -/* - store.dispatch({ - type: ADD_ALERT, - alert: { - type: "REQUEST_FORBIDDEN", - title: "Request failed due to insufficient permissions", - content: , - }, - }) -*/ const contentType = ctx.response.headers.get("content-type") if (contentType === "application/json") { return ctx.response.json().then((body: any) => Promise.reject(body)) @@ -313,9 +284,6 @@ export function updateSubscription(watch: Watch, alerting: AlertContextType) : P return apiCall(subscriptionsApi.updateSubscription(watch.testId, watch), alerting, "SUBSCRIPTION_UPDATE", "Failed to update subscription"); } - - - export function updateTransformers(testId: number, transformers: Transformer[], alerting: AlertContextType) : Promise { return apiCall(testApi.updateTransformers(testId, transformers.map(t => t.id)), alerting, "UPDATE_TRANSFORMERS", "Failed to update transformers for test " + testId); } diff --git a/horreum-web/src/auth.tsx b/horreum-web/src/auth.tsx deleted file mode 100644 index 2f59107b8..000000000 --- a/horreum-web/src/auth.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { useDispatch, useSelector } from "react-redux" - -import { Button } from "@patternfly/react-core" - -import { State } from "./store" -import { UserData } from "./api" -import { ThunkDispatch } from "redux-thunk" -import Keycloak, {KeycloakProfile} from "keycloak-js"; -import {useState} from "react"; -import LoginModal from "./Login"; - -export const INIT = "auth/INIT" -export const UPDATE_DEFAULT_TEAM = "auth/UPDATE_DEFAULT_TEAM" -export const UPDATE_ROLES = "auth/UPDATE_ROLES" -export const STORE_PROFILE = "auth/STORE_PROFILE" -export const BASIC_AUTH = "auth/BASIC_AUTH" -export const AFTER_LOGOUT = "auth/AFTER_LOGOUT" - -export class AuthState { - basicAuthToken? : string = undefined - keycloak?: Keycloak - authenticated = false - roles: string[] = [] - teams: string[] = [] - defaultTeam?: string = undefined - userProfile?: KeycloakProfile - initPromise?: Promise = undefined -} - -interface InitAction { - type: typeof INIT - keycloak?: Keycloak - initPromise?: Promise -} - -export interface UpdateDefaultTeamAction { - type: typeof UPDATE_DEFAULT_TEAM - team?: string -} - -interface UpdateRolesAction { - type: typeof UPDATE_ROLES - authenticated: boolean - roles: string[] -} - -interface StoreProfileAction { - type: typeof STORE_PROFILE - profile?: KeycloakProfile -} - -interface BasicAuthAction { - type: typeof BASIC_AUTH - username?: string - password?: string -} - -interface AfterLogoutAction { - type: typeof AFTER_LOGOUT -} - -type AuthAction = InitAction | UpdateDefaultTeamAction | UpdateRolesAction | StoreProfileAction | BasicAuthAction | AfterLogoutAction - -export type AuthDispatch = ThunkDispatch - -export function reducer(state : AuthState = { authenticated : false, roles : [], teams : []}, action: AuthAction) { -// export function reducer(state = new AuthState(), action: AuthAction) { - // TODO: is this necessary? It seems that without that the state is not updated at times. - state = { ...state } - switch (action.type) { - case "auth/INIT": - state.keycloak = action.keycloak - if (action.initPromise) { - state.initPromise = action.initPromise - } - break - case "auth/UPDATE_DEFAULT_TEAM": - state.defaultTeam = action.team - break - case "auth/UPDATE_ROLES": - state.authenticated = action.authenticated - state.roles = [...action.roles] - state.teams = action.roles.filter(role => role.endsWith("-team")).sort() - break - case "auth/STORE_PROFILE": - state.userProfile = action.profile - break - case "auth/BASIC_AUTH": - state.basicAuthToken = window.btoa(action.username + ':' + action.password) - break - case "auth/AFTER_LOGOUT": - state.basicAuthToken = undefined - state.userProfile = undefined - state.initPromise = undefined - state.authenticated = false - state.roles = [] - state.teams = [] - state.defaultTeam = undefined - break - default: - } - return state -} - -export const keycloakSelector = (state: State) => { - return state.auth.keycloak -} - -export const oidcSelector = (state: State) => { - return state.auth.keycloak?.authServerUrl -} - -export const tokenSelector = (state: State) => { - return state.auth.keycloak && state.auth.keycloak.token -} - -export const teamToName = (team?: string) => { - return team ? team.charAt(0).toUpperCase() + team.slice(1, -5) : undefined -} - -export const userProfileSelector = (state: State) => { - return state.auth.userProfile -} - -export const isAuthenticatedSelector = (state: State) => { - return state.auth.authenticated -} - -export const isAdminSelector = (state: State) => { - return state.auth.roles.includes("admin") -} - -export const isManagerSelector = (state: State) => { - return managedTeamsSelector(state).length > 0 -} - -export const teamsSelector = (state: State): string[] => { - return state.auth.teams -} - -function rolesSelector(state: State) { - return state.auth.roles -} - -function isTester(owner: string, roles: string[]) { - return roles.includes(owner.slice(0, -4) + "tester") -} - -export function useTester(owner?: string) { - const roles = useSelector(rolesSelector) - return roles.includes("tester") && (!owner || isTester(owner, roles)) -} - -export function managedTeamsSelector(state: State) { - return state.auth.roles.filter(role => role.endsWith("-manager")).map(role => role.slice(0, -7) + "team") -} - -export function useManagedTeams(): string[] { - return useSelector(managedTeamsSelector) -} - -export const defaultTeamSelector = (state: State) => { - if (state.auth.defaultTeam !== undefined) { - return state.auth.defaultTeam - } - const teamRoles = teamsSelector(state) - return teamRoles.length > 0 ? teamRoles[0] : undefined -} - -export const TryLoginAgain = () => { - const keycloak = useSelector(keycloakSelector) - return keycloak ? ( - <> - Try{" "} - - - ) : null -} - -export const LoginLogout = () => { - const oidc = useSelector(oidcSelector) - const keycloak = useSelector(keycloakSelector) - // for some reason isAuthenticatedSelector would not return correct value at times (Redux bug?) - const authenticated = useSelector(isAuthenticatedSelector) - const [loginModalOpen, setLoginModalOpen] = useState(false) - const dispatch = useDispatch() - if (!keycloak) { - return - } - if (authenticated) { - return ( - - ) - } else { - return <> - - setLoginModalOpen(false)} - /> - - } -} - - -export function userName(user: UserData) { - let str = "" - if (user.firstName) { - str += user.firstName + " " - } - if (user.lastName) { - str += user.lastName + " " - } - if (user.firstName || user.lastName) { - return str + " [" + user.username + "]" - } else { - return user.username - } -} diff --git a/horreum-web/src/auth/CallbackSSO.tsx b/horreum-web/src/auth/CallbackSSO.tsx new file mode 100644 index 000000000..15a375da8 --- /dev/null +++ b/horreum-web/src/auth/CallbackSSO.tsx @@ -0,0 +1,34 @@ +import {Bullseye, Spinner} from "@patternfly/react-core"; +import {useAuth} from "react-oidc-context"; +import {useNavigate} from "react-router-dom"; +import {useEffect, useState} from "react"; + +// Default callback sso component, which redirects to the last visited page when the login is completed +function CallbackSSO() { + const auth = useAuth() + const navigate = useNavigate(); + + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // redirect only once the authentication stopped loading + if (!isLoading) { + const history = (auth.user?.state as { history?: string }).history ?? "/"; + navigate(history, {replace: true}); + } + }, [isLoading]); + + if (!auth.isLoading) { + if (isLoading) { + setIsLoading(false); + } + } + + return ( + + + + ); +} + +export default CallbackSSO; diff --git a/horreum-web/src/auth/auth.tsx b/horreum-web/src/auth/auth.tsx new file mode 100644 index 000000000..35307d3d9 --- /dev/null +++ b/horreum-web/src/auth/auth.tsx @@ -0,0 +1,43 @@ +import { Button } from "@patternfly/react-core" + +import React, {useContext, useState} from "react"; +import LoginModal from "../Login"; +import {AuthBridgeContext} from "../context/AuthBridgeContext"; +import {AuthContextType} from "../context/@types/authContextTypes"; + + +export const LoginLogout = () => { + const { isOidc, isAuthenticated, signIn, signOut } = useContext(AuthBridgeContext) as AuthContextType; + + const [loginModalOpen, setLoginModalOpen] = useState(false) + + if (isAuthenticated) { + return ( + + ) + } else { + return <> + + setLoginModalOpen(false)} + /> + + } +} diff --git a/horreum-web/src/auth/oidc.ts b/horreum-web/src/auth/oidc.ts new file mode 100644 index 000000000..c407f1a9d --- /dev/null +++ b/horreum-web/src/auth/oidc.ts @@ -0,0 +1,14 @@ +import { KeycloakConfig } from "../generated"; +import { UserManager, WebStorageStateStore } from "oidc-client-ts"; + +export const createUserManager = (config: KeycloakConfig): UserManager => { + return new UserManager({ + authority: config.url + "/realms/" + config.realm, + client_id: config.clientId ?? "", + redirect_uri: `${window.location.origin}/callback-sso`, + post_logout_redirect_uri: window.location.origin, + userStore: new WebStorageStateStore({ store: window.localStorage }), + monitorSession: true, + automaticSilentRenew: true, + }) +} diff --git a/horreum-web/src/components/ActionMenu.tsx b/horreum-web/src/components/ActionMenu.tsx index 24cff15f4..4fa435741 100644 --- a/horreum-web/src/components/ActionMenu.tsx +++ b/horreum-web/src/components/ActionMenu.tsx @@ -1,14 +1,14 @@ -import { useState, useEffect, ReactElement, ReactNode } from "react" +import {useState, useEffect, ReactElement, ReactNode, useContext} from "react" -import { useTester } from "../auth" import { Access } from "../api" -import ShareLinkModal from "./ShareLinkModal" import ChangeAccessModal from "./ChangeAccessModal" import ConfirmDeleteModal from "./ConfirmDeleteModal" import { Dropdown, DropdownItem, MenuToggle, MenuToggleElement } from "@patternfly/react-core" import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import {AuthBridgeContext} from "../context/AuthBridgeContext"; +import {AuthContextType} from "../context/@types/authContextTypes"; interface MenuItemProvider { (props: ActionMenuProps, isTester: boolean, close: () => void, config: C): { @@ -27,47 +27,6 @@ export type ActionMenuProps = { items: MenuItem[] } -type ShareLinkConfig = { - token?: string - tokenToLink(id: number, token: string): string - onTokenReset(id: number): void - onTokenDrop(id: number): void -} - -export function useShareLink(config: ShareLinkConfig): MenuItem { - const [shareLinkModalOpen, setShareLinkModalOpen] = useState(false) - return [ - (props: ActionMenuProps, isTester: boolean, close: () => void, config: ShareLinkConfig) => { - return { - item: ( - { - close() - setShareLinkModalOpen(true) - }} - > - Shareable link - - ), - modal: ( - setShareLinkModalOpen(false)} - isTester={isTester} - link={config.token ? config.tokenToLink(props.id, config.token) : ""} - onReset={() => config.onTokenReset(props.id)} - onDrop={() => config.onTokenDrop(props.id)} - /> - ), - } - }, - config, - ] -} - type ChangeAccessConfig = { onAccessUpdate(id: number, owner: string, access: Access): void } @@ -173,13 +132,13 @@ export function useDelete(config: DeleteConfig): MenuItem { } export default function ActionMenu(props: ActionMenuProps) { + const { isTester } = useContext(AuthBridgeContext) as AuthContextType; const [menuOpen, setMenuOpen] = useState(false) - const isTester = useTester(props.owner) const onSelect = () => { setMenuOpen(false); }; - const items = props.items.map(([provider, config]) => provider(props, isTester, () => setMenuOpen(false), config)) + const items = props.items.map(([provider, config]) => provider(props, isTester(), () => setMenuOpen(false), config)) return ( <> ([]) const [typeaheadOptions, setTypeaheadOptions] = useState([]) - const teams = useSelector(teamsSelector) useEffect(() => { fetchFolders(alerting).then(folders => { // the root folder is represented by a null object in the returned list diff --git a/horreum-web/src/components/FoldersAccordian.tsx b/horreum-web/src/components/FoldersAccordian.tsx index 3a332302f..1366d1751 100644 --- a/horreum-web/src/components/FoldersAccordian.tsx +++ b/horreum-web/src/components/FoldersAccordian.tsx @@ -1,14 +1,14 @@ import {useContext, useEffect, useState} from "react" -import { useDispatch, useSelector } from "react-redux" import { Accordion, AccordionItem, AccordionContent, AccordionToggle } from '@patternfly/react-core'; -import { TreeView, TreeViewDataItem } from "@patternfly/react-core" +import { TreeViewDataItem } from "@patternfly/react-core" -import { teamsSelector } from "../auth" import {fetchFolders} from "../api"; -import {AppContext} from "../context/appContext"; +import {AppContext} from "../context/AppContext"; import {AppContextType} from "../context/@types/appContextTypes"; +import {AuthBridgeContext} from "../context/AuthBridgeContext"; +import {AuthContextType} from "../context/@types/authContextTypes"; type FoldersAccordianProps = { folder: string @@ -17,8 +17,8 @@ type FoldersAccordianProps = { export default function FoldersAccordian(props: FoldersAccordianProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { teams } = useContext(AuthBridgeContext) as AuthContextType; const [folders, setFolders] = useState([]) - const teams = useSelector(teamsSelector) const [expanded, setExpanded] = useState('') useEffect(() => { diff --git a/horreum-web/src/components/HttpActionUrlSelector.tsx b/horreum-web/src/components/HttpActionUrlSelector.tsx index ce2ed4ad6..9dadf4791 100644 --- a/horreum-web/src/components/HttpActionUrlSelector.tsx +++ b/horreum-web/src/components/HttpActionUrlSelector.tsx @@ -10,7 +10,7 @@ import { } from '@patternfly/react-core'; import { AllowedSite, getAllowedSites} from "../api" -import {AppContext} from "../context/appContext"; +import {AppContext} from "../context/AppContext"; import {AppContextType} from "../context/@types/appContextTypes"; import { SimpleSelect } from "./templates/SimpleSelect"; diff --git a/horreum-web/src/components/ImportButton.tsx b/horreum-web/src/components/ImportButton.tsx index 997bd083c..f323d17da 100644 --- a/horreum-web/src/components/ImportButton.tsx +++ b/horreum-web/src/components/ImportButton.tsx @@ -2,7 +2,7 @@ import {useContext, useState} from "react" import {Bullseye, Button, FileUpload, Spinner} from '@patternfly/react-core'; import {Modal} from '@patternfly/react-core/deprecated'; -import {AppContext} from "../context/appContext"; +import {AppContext} from "../context/AppContext"; import {AppContextType} from "../context/@types/appContextTypes"; import {SchemaExport, TableReportConfig, TestExport} from "../generated"; diff --git a/horreum-web/src/components/LabelFilter/LabelFilter.tsx b/horreum-web/src/components/LabelFilter/LabelFilter.tsx index 07985aaa6..43de09776 100644 --- a/horreum-web/src/components/LabelFilter/LabelFilter.tsx +++ b/horreum-web/src/components/LabelFilter/LabelFilter.tsx @@ -1,6 +1,4 @@ -import React, { CSSProperties, ReactElement, useEffect, useMemo, useState } from "react" -import { useSelector } from "react-redux" -import { teamsSelector } from "../../auth" +import React, {CSSProperties, useContext, useEffect, useMemo, useState} from "react" import { FlexItem, @@ -10,10 +8,12 @@ import { ToolbarToggleGroup } from '@patternfly/react-core'; import { BanIcon } from "@patternfly/react-icons" -import { deepEquals, noop } from "../../utils" +import { noop } from "../../utils" import FilterIcon from "@patternfly/react-icons/dist/esm/icons/filter-icon"; import ToolbarLabelFilter from "../ToolbarLabelFilter"; import FilterDropDown from "./FilterDropDown"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; function convertLabelValue(value: any) { if (typeof value === "object") { @@ -37,7 +37,7 @@ type LabelsSelectProps = { } export default function LabelFilter({selection, onSelect, source, clearCallback}: LabelsSelectProps) { - const teams = useSelector(teamsSelector) + const { teams } = useContext(AuthBridgeContext) as AuthContextType; const [availableLabels, setAvailableLabels] = useState({}) useEffect(() => { diff --git a/horreum-web/src/components/Labels.tsx b/horreum-web/src/components/Labels.tsx index 38dba4495..9c69c57e2 100644 --- a/horreum-web/src/components/Labels.tsx +++ b/horreum-web/src/components/Labels.tsx @@ -7,7 +7,7 @@ import {ExclamationCircleIcon} from "@patternfly/react-icons" import {LabelInfo, schemaApi} from "../api" -import {AppContext} from "../context/appContext"; +import {AppContext} from "../context/AppContext"; import {AppContextType} from "../context/@types/appContextTypes"; import FilterIcon from "@patternfly/react-icons/dist/esm/icons/filter-icon"; import { TypeaheadSelect } from "./templates/TypeahedSelect" diff --git a/horreum-web/src/components/LabelsSelect.tsx b/horreum-web/src/components/LabelsSelect.tsx index e92bdc2f5..b1e643d35 100644 --- a/horreum-web/src/components/LabelsSelect.tsx +++ b/horreum-web/src/components/LabelsSelect.tsx @@ -1,6 +1,4 @@ -import { CSSProperties, ReactElement, useEffect, useMemo, useState } from "react" -import { useSelector } from "react-redux" -import { teamsSelector } from "../auth" +import {CSSProperties, ReactElement, useContext, useEffect, useMemo, useState} from "react" import { Button, @@ -64,6 +62,7 @@ type LabelsSelectProps = { } export default function LabelsSelect({disabled, selection, onSelect, source, emptyPlaceholder, fireOnPartial, showKeyHelper, addResetButton, style}: LabelsSelectProps) { + const { teams } = useContext(AuthBridgeContext) as AuthContextType; const [availableLabels, setAvailableLabels] = useState([]) const initialSelect = selection ? Object.entries(selection).reduce((acc, [key, value]) => { @@ -75,7 +74,6 @@ export default function LabelsSelect({disabled, selection, onSelect, source, emp : {} const [partialSelect, setPartialSelect] = useState(initialSelect) - const teams = useSelector(teamsSelector) useEffect(() => { source().then((response: any[]) => { setAvailableLabels(response) diff --git a/horreum-web/src/components/LogModal.tsx b/horreum-web/src/components/LogModal.tsx index 47a9dfc5f..fc3d20878 100644 --- a/horreum-web/src/components/LogModal.tsx +++ b/horreum-web/src/components/LogModal.tsx @@ -19,7 +19,7 @@ import { import TimeRangeSelect, { TimeRange } from "./TimeRangeSelect" import ConfirmDeleteModal from "./ConfirmDeleteModal" -import {AppContext} from "../context/appContext"; +import {AppContext} from "../context/AppContext"; import {AppContextType} from "../context/@types/appContextTypes"; import {IRow, IRowCell, OuterScrollContainer, Table, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; import { SimpleSelect } from "./templates/SimpleSelect" diff --git a/horreum-web/src/components/OwnerAccess.tsx b/horreum-web/src/components/OwnerAccess.tsx index baa5c0c82..25aca02ae 100644 --- a/horreum-web/src/components/OwnerAccess.tsx +++ b/horreum-web/src/components/OwnerAccess.tsx @@ -2,7 +2,7 @@ import { useState } from "react" import { Button } from "@patternfly/react-core" import { EditIcon } from "@patternfly/react-icons" -import { teamToName } from "../auth" +import { teamToName } from "../utils" import { Access } from "../api" import AccessIcon from "./AccessIcon" import ChangeAccessModal from "./ChangeAccessModal" diff --git a/horreum-web/src/components/TeamSelect.tsx b/horreum-web/src/components/TeamSelect.tsx index 77fc464db..634911624 100644 --- a/horreum-web/src/components/TeamSelect.tsx +++ b/horreum-web/src/components/TeamSelect.tsx @@ -1,8 +1,6 @@ -import {Ref, useState} from "react" -import {State} from "../store" -import {useSelector} from "react-redux" +import {Ref, useContext, useState} from "react" -import {isAuthenticatedSelector, teamsSelector as allTeamsSelector, teamToName} from "../auth" +import {teamToName} from "../utils" import { Divider, MenuToggle, @@ -12,6 +10,8 @@ import { SelectList, SelectOption, } from "@patternfly/react-core"; +import {AuthBridgeContext} from "../context/AuthBridgeContext"; +import {AuthContextType} from "../context/@types/authContextTypes"; export interface Team { key: string @@ -35,17 +35,18 @@ export const SHOW_ALL: Team = {key: "__all", toString: () => "Show all"} type TeamSelectProps = { includeGeneral: boolean selection: string | Team - teamsSelector?(state: State): string[] + selectedTeams?: string[] onSelect(selection: Team): void } -export default function TeamSelect({includeGeneral, selection, teamsSelector, onSelect}: TeamSelectProps) { +export default function TeamSelect({includeGeneral, selection, selectedTeams, onSelect}: TeamSelectProps) { + const { isAuthenticated, teams: allTeams } = useContext(AuthBridgeContext) as AuthContextType; const [isOpen, setIsOpen] = useState(false); - const teams = useSelector(teamsSelector || allTeamsSelector) + const teams = selectedTeams || allTeams const generalOptions = () => - {(useSelector(isAuthenticatedSelector) ? [SHOW_ALL, ONLY_MY_OWN] : [SHOW_ALL]) + {(isAuthenticated ? [SHOW_ALL, ONLY_MY_OWN] : [SHOW_ALL]) .map(t => {t.toString()})} diff --git a/horreum-web/src/components/TestSelect.tsx b/horreum-web/src/components/TestSelect.tsx index d9c0ba743..dd16ab172 100644 --- a/horreum-web/src/components/TestSelect.tsx +++ b/horreum-web/src/components/TestSelect.tsx @@ -10,7 +10,7 @@ import { SplitItem } from '@patternfly/react-core'; import {fetchTests, Test} from "../api" -import {AppContext} from "../context/appContext"; +import {AppContext} from "../context/AppContext"; import {AppContextType} from "../context/@types/appContextTypes"; import {useSelector} from "react-redux"; import {teamsSelector} from "../auth"; @@ -57,7 +57,7 @@ function groupByFolder(tests: Test[] | undefined | false) { export default function TestSelect(props: TestSelectProps) { const { alerting } = useContext(AppContext) as AppContextType; - const teams = useSelector(teamsSelector) + const { teams } = useContext(AuthBridgeContext) as AuthContextType; const [testList, setTestList] = useState() useMemo(() => { fetchTests(alerting, undefined, "*") diff --git a/horreum-web/src/components/UserSearch.tsx b/horreum-web/src/components/UserSearch.tsx index 0aadd658d..65eeb693d 100644 --- a/horreum-web/src/components/UserSearch.tsx +++ b/horreum-web/src/components/UserSearch.tsx @@ -2,7 +2,7 @@ import {useContext, useState} from "react" import { Button, Flex, FlexItem, SearchInput } from "@patternfly/react-core" import { ArrowRightIcon } from "@patternfly/react-icons" import {userApi, UserData} from "../api" -import {AppContext} from "../context/appContext"; +import {AppContext} from "../context/AppContext"; import {AppContextType} from "../context/@types/appContextTypes"; type UserSearchProps = { diff --git a/horreum-web/src/context/@types/appContextTypes.ts b/horreum-web/src/context/@types/appContextTypes.ts index 4ccbf3218..2b05c0f5b 100644 --- a/horreum-web/src/context/@types/appContextTypes.ts +++ b/horreum-web/src/context/@types/appContextTypes.ts @@ -18,14 +18,6 @@ export type AlertContextType = { } -export type AuthContextType = { - updateDefaultTeam: (team: string, - onSuccess: () => void, - onFailure: (error: any) => void) => Promise; -} - - export type AppContextType = { alerting: AlertContextType; - auth: AuthContextType; -}; +}; \ No newline at end of file diff --git a/horreum-web/src/context/@types/authContextTypes.ts b/horreum-web/src/context/@types/authContextTypes.ts new file mode 100644 index 000000000..ca0ae6b4b --- /dev/null +++ b/horreum-web/src/context/@types/authContextTypes.ts @@ -0,0 +1,18 @@ + +export type AuthContextType = { + isOidc: boolean; + isAuthenticated: boolean; + name?: string; + username?: string; + token?: string; + roles: string[]; + teams: string[]; + managedTeams: string[]; + defaultTeam?: string; + isTester: (owner?: string) => boolean; + isManager: () => boolean; + isAdmin: () => boolean; + // login / logout + signIn: (username?: string, password?: string) => Promise; + signOut: () => Promise; +} \ No newline at end of file diff --git a/horreum-web/src/context/appContext.tsx b/horreum-web/src/context/AppContext.tsx similarity index 73% rename from horreum-web/src/context/appContext.tsx rename to horreum-web/src/context/AppContext.tsx index bc58c6830..fec01500a 100644 --- a/horreum-web/src/context/appContext.tsx +++ b/horreum-web/src/context/AppContext.tsx @@ -2,17 +2,17 @@ import * as React from 'react'; import {Alert, contextAlertAction} from "../alerts"; import {useState} from "react"; import {AlertVariant} from "@patternfly/react-core"; -import {userApi} from "../api"; -import {AlertContextType, AppContextType, AuthContextType} from "./@types/appContextTypes"; +import {AlertContextType, AppContextType} from "./@types/appContextTypes"; export const AppContext = React.createContext(null); -type ContextProviderProps = { +type AppContextProviderProps = { children: React.ReactNode; }; -const ContextProvider: React.FC = ({ children }) => { +const AppContextProvider: React.FC = ({ children }) => { const [alerts, setAlerts] = useState([]); + const newAlert = (newAlert: Alert) => { setAlerts([...alerts, newAlert]); }; @@ -55,24 +55,11 @@ const ContextProvider: React.FC = ({ children }) => { dispatchInfo: contextDispatchInfo }; - const updateDefaultTeam = (team: string, onSuccess: () => void, onFailure: (error: any) => void): Promise => { - return userApi.setDefaultTeam(team).then( - _ => onSuccess(), - error => onFailure(error) - ) - } - - - const auth : AuthContextType = { - updateDefaultTeam: updateDefaultTeam - }; - const context : AppContextType = { alerting: alerting, - auth: auth }; return {children}; }; -export default ContextProvider; +export default AppContextProvider; diff --git a/horreum-web/src/context/AuthBridgeContext.tsx b/horreum-web/src/context/AuthBridgeContext.tsx new file mode 100644 index 000000000..deeef6a02 --- /dev/null +++ b/horreum-web/src/context/AuthBridgeContext.tsx @@ -0,0 +1,152 @@ +// This effect syncs the React auth state to your static AuthService +import * as React from "react"; +import {AuthContextType} from "./@types/authContextTypes"; +import {useAuth} from "react-oidc-context"; +import {useEffect, useState} from "react"; +import {userApi, UserData} from "../api"; +import {Bullseye, Spinner} from "@patternfly/react-core"; + +// this simple object acts as a bridge for api middleware. +export interface IAuthBridge { + isOidc: boolean; + accessToken: string | undefined; + setToken: (token: string | undefined) => void; + setIsOidc: (isOidc: boolean) => void; +} + +export const AuthBridge: IAuthBridge = { + isOidc: true, + accessToken: undefined, + setToken: (token) => { + AuthBridge.accessToken = token; + }, + setIsOidc: (isOidc) => { + AuthBridge.isOidc = isOidc; + }, +}; + +function isTester(owner: string, roles: string[]) { + return roles.includes(owner.slice(0, -4) + "tester") +} + +export const AuthBridgeContext = React.createContext(null); + +const signOutCallback = () => window.location.replace(window.location.origin) + +type ContextProviderProps = { + isOidc: boolean; + children: React.ReactNode; +}; + +const AuthBridgeContextProvider: React.FC = ({ isOidc, children }) => { + const auth = useAuth(); + + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [name, setName] = useState(); + const [username, setUsername] = useState(); + const [token, setToken] = useState(); + const [isBridgeTokenSet, setIsBridgeTokenSet] = useState(false); + const [roles, setRoles] = useState([]); + const [defaultTeam, setDefaultTeam] = useState(undefined); + + // set the isOidc property bridge to let the api middleware fetch it + AuthBridge.setIsOidc(isOidc) + + const signIn = isOidc ? + () => { + // save the current pathname in the local session to let the callback page redirect back + return auth.signinRedirect({state: { history: `${location.pathname}${location.search}` }}) + } : + (username?: string, password?: string) => { + setIsAuthenticated(true) + setToken(window.btoa(username + ':' + password)) + setUsername(username) + return Promise.resolve() + }; + + const signOut = isOidc ? + () => auth.removeUser().then(() => window.localStorage.clear()).then(() => auth.clearStaleState()).then(signOutCallback) : + () => { + setToken(undefined) + setIsAuthenticated(false) + return Promise.resolve().then(signOutCallback) + }; + + useEffect(() => { + if (isOidc) { + setIsAuthenticated(auth.isAuthenticated); + setToken(auth.user?.access_token) + setUsername(auth.user?.profile.preferred_username) + setName(auth.user?.profile.name) + } + }, [auth.isAuthenticated]); + + useEffect(() => { + AuthBridge.setToken(token); + setIsBridgeTokenSet(token !== undefined); + }, [token]); + + useEffect(() => { + if (isBridgeTokenSet) { + userApi.getRoles().then( + roles => { + setRoles(roles); + userApi.defaultTeam().then( + team => setDefaultTeam(team || undefined), + error => console.log(error) + ) + + if (!isOidc) { + // we also need to fetch the user data as not coming from oidc + userApi.searchUsers(username!).then( + userData => { + const user: UserData | undefined = userData.filter(u => u.username == username).at(0) + setName(user?.firstName + " " + user?.lastName) + }, + error => console.log(error) + ) + } + }, + error => console.log(error) + ) + } else { + // reset + setRoles([]) + } + }, [isBridgeTokenSet]); + + const teams = roles.filter(role => role.endsWith("-team")).sort() + const managedTeams = roles.filter(role => role.endsWith("-manager")).map(role => role.slice(0, -7) + "team") + + const authCtx : AuthContextType = { + isOidc: isOidc, + isAuthenticated: isAuthenticated, + name: name, + username: username, + token: token, + roles: roles, + teams: teams, + managedTeams: managedTeams, + defaultTeam: defaultTeam ?? (teams.length > 0 ? teams[0] : undefined), + isTester: (owner?: string) => roles.includes("tester") && (!owner || isTester(owner, roles)), + isManager: () => managedTeams.length > 0, + isAdmin: () => roles.includes("admin"), + signIn: signIn, + signOut: signOut, + }; + + // if (isOidc && auth.isLoading) { + // // we need this to let auth properly complete the authentication + // // otherwise we are too fast + // return ( + // + // + // + // ) + // } + + return {children}; +}; + +export default AuthBridgeContextProvider; + diff --git a/horreum-web/src/domain/actions/ActionList.tsx b/horreum-web/src/domain/actions/ActionList.tsx index dea1e6c1b..aabc30868 100644 --- a/horreum-web/src/domain/actions/ActionList.tsx +++ b/horreum-web/src/domain/actions/ActionList.tsx @@ -1,34 +1,34 @@ import {useContext, useEffect, useMemo, useState} from "react" -import { useSelector } from "react-redux" import { Button, Hint, HintBody, PageSection, Switch, Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core" import {allActions, addGlobalAction, deleteGlobalAction} from "../../api" -import { isAdminSelector } from "../../auth" import AddActionModal from "./AddActionModal" import {Action} from "../../api" import ActionLogModal from "../tests/ActionLogModal" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import CustomTable from "../../components/CustomTable" import { ColumnDef, createColumnHelper } from "@tanstack/react-table" +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; const columnHelper = createColumnHelper() export default function ActionList() { const { alerting } = useContext(AppContext) as AppContextType; + const { isAdmin } = useContext(AuthBridgeContext) as AuthContextType; const [logOpen, setLogOpen] = useState(false) const [isOpen, setOpen] = useState(false) const [actions, setActions] = useState([]) - const isAdmin = useSelector(isAdminSelector) const fetchAllActions = () => { allActions(alerting).then(setActions) } useEffect(() => { - if (isAdmin) { + if (isAdmin()) { fetchAllActions() } }, [isAdmin]) diff --git a/horreum-web/src/domain/actions/AllowedSiteList.tsx b/horreum-web/src/domain/actions/AllowedSiteList.tsx index a6ddd9635..ceb694efd 100644 --- a/horreum-web/src/domain/actions/AllowedSiteList.tsx +++ b/horreum-web/src/domain/actions/AllowedSiteList.tsx @@ -5,7 +5,7 @@ import { Bullseye, Button, Spinner, Toolbar, ToolbarContent, ToolbarItem } from import { addSite, AllowedSite, deleteSite, getAllowedSites} from "../../api" import AddAllowedSiteModal from "./AddAllowedSiteModal" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import CustomTable from "../../components/CustomTable" import { ColumnDef, createColumnHelper } from "@tanstack/react-table" diff --git a/horreum-web/src/domain/admin/Admin.tsx b/horreum-web/src/domain/admin/Admin.tsx index 9ff035e29..9af2d9591 100644 --- a/horreum-web/src/domain/admin/Admin.tsx +++ b/horreum-web/src/domain/admin/Admin.tsx @@ -1,4 +1,4 @@ -import {useRef} from "react" +import {useContext, useRef} from "react" import {Card, CardBody, PageSection} from "@patternfly/react-core" import SavedTabs, {SavedTab, TabFunctions, saveFunc, resetFunc, modifiedFunc} from "../../components/SavedTabs" @@ -9,16 +9,17 @@ import BannerConfig from "./BannerConfig" import Notifications from "./Notifications" import Teams from "./Teams" import Administrators from "./Administrators" -import {useSelector} from "react-redux"; -import {isAdminSelector, isManagerSelector} from "../../auth"; import Datastores from "./Datastores"; import RemoveUsers from "./RemoveUsers"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; export default function Admin() { + const { isManager: isManagerFunc, isAdmin: isAdminFunc } = useContext(AuthBridgeContext) as AuthContextType; const adminFuncsRef = useRef(undefined) const teamsFuncsRef = useRef(undefined) - const isAdmin = useSelector(isAdminSelector) - const isManager = useSelector(isManagerSelector) + const isAdmin = isAdminFunc() + const isManager = isManagerFunc() if (isAdmin) { return ( diff --git a/horreum-web/src/domain/admin/Administrators.tsx b/horreum-web/src/domain/admin/Administrators.tsx index 820e6aaba..508348b95 100644 --- a/horreum-web/src/domain/admin/Administrators.tsx +++ b/horreum-web/src/domain/admin/Administrators.tsx @@ -1,16 +1,17 @@ import {ReactElement, useState, useEffect, useContext} from "react" -import { useSelector } from "react-redux" import {Button, Form, FormGroup} from '@patternfly/react-core'; import {DualListSelector} from '@patternfly/react-core/deprecated'; import { TabFunctionsRef } from "../../components/SavedTabs" import {userApi, UserData} from "../../api" import UserSearch from "../../components/UserSearch" -import {isAdminSelector, userName} from "../../auth" -import {AppContext} from "../../context/appContext"; +import { userName } from "../../utils" +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import NewUserModal from "../user/NewUserModal"; import {noop} from "../../utils"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; function userElement(u: UserData) { @@ -27,14 +28,15 @@ type AdministratorsProps = { export default function Administrators(props: AdministratorsProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { isAdmin } = useContext(AuthBridgeContext) as AuthContextType; const [modified, setModified] = useState(false) const [resetCounter, setResetCounter] = useState(0) const [createNewUser, setCreateNewUser] = useState(false) const [availableUsers, setAvailableUsers] = useState[]>([]) const [admins, setAdmins] = useState[]>([]) - const isAdmin = useSelector(isAdminSelector) + useEffect(() => { - if (isAdmin) { + if (isAdmin()) { userApi.administrators().then( list => setAdmins(list.map(userElement)), error => alerting.dispatchError(error, "FETCH ADMINS", "Cannot fetch administrators") diff --git a/horreum-web/src/domain/admin/BannerConfig.tsx b/horreum-web/src/domain/admin/BannerConfig.tsx index f3ca2b587..fd49d8446 100644 --- a/horreum-web/src/domain/admin/BannerConfig.tsx +++ b/horreum-web/src/domain/admin/BannerConfig.tsx @@ -1,5 +1,4 @@ import {useContext, useEffect, useState} from "react" -import { useSelector } from "react-redux" import { ActionGroup, @@ -16,10 +15,11 @@ import { } from "@patternfly/react-core" import Editor from "../../components/Editor/monaco/Editor" -import { isAdminSelector } from "../../auth" import {bannerApi} from "../../api" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; function setBanner(severity: string, title: string, message: string) { return bannerApi.setBanner({ severity, title, message, active: true }) @@ -27,11 +27,12 @@ function setBanner(severity: string, title: string, message: string) { export default function BannerConfig() { const { alerting } = useContext(AppContext) as AppContextType; + const { isAdmin } = useContext(AuthBridgeContext) as AuthContextType; const [severity, setSeverity] = useState("danger") const [title, setTitle] = useState("") const [message, setMessage] = useState("") const [saving, setSaving] = useState(false) - const isAdmin = useSelector(isAdminSelector) + useEffect(() => { bannerApi.getBanner().then( banner => { @@ -45,7 +46,7 @@ export default function BannerConfig() { ) }, []) document.title = "Banner | Horreum" - if (!isAdmin) { + if (!isAdmin()) { return null } return ( diff --git a/horreum-web/src/domain/admin/Datastores.tsx b/horreum-web/src/domain/admin/Datastores.tsx index ed7376ede..f00dc9174 100644 --- a/horreum-web/src/domain/admin/Datastores.tsx +++ b/horreum-web/src/domain/admin/Datastores.tsx @@ -27,10 +27,10 @@ import { DatastoreTypeEnum, ElasticsearchDatastoreConfig, TypeConfig } from "../../api"; -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; -import {useSelector} from "react-redux"; -import {defaultTeamSelector} from "../../auth"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; interface dataStoreTableProps { datastores: Datastore[] @@ -125,10 +125,10 @@ const errorFormatter = (error: any) => { export default function Datastores() { const {alerting} = useContext(AppContext) as AppContextType; - const defaultTeam = useSelector(defaultTeamSelector) || SHOW_ALL.key; + const { defaultTeam } = useContext(AuthBridgeContext) as AuthContextType; const [datastores, setDatastores] = useState([]) const [datastoreTypes, setDatastoreTypes] = useState([]) - const [curTeam, setCurTeam] = useState(createTeam(defaultTeam)); + const [curTeam, setCurTeam] = useState(createTeam(defaultTeam || SHOW_ALL.key)); const newDataStore: Datastore = { id: -1, diff --git a/horreum-web/src/domain/admin/Notifications.tsx b/horreum-web/src/domain/admin/Notifications.tsx index 31c878e28..751d67b55 100644 --- a/horreum-web/src/domain/admin/Notifications.tsx +++ b/horreum-web/src/domain/admin/Notifications.tsx @@ -3,7 +3,7 @@ import {useContext, useState} from "react" import { ActionGroup, Button, Form, FormGroup, TextInput } from "@patternfly/react-core" import NotificationMethodSelect from "../../components/NotificationMethodSelect" import { notificationsApi } from "../../api" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; diff --git a/horreum-web/src/domain/admin/RemoveUsers.tsx b/horreum-web/src/domain/admin/RemoveUsers.tsx index 0a820d778..8bb1ac95c 100644 --- a/horreum-web/src/domain/admin/RemoveUsers.tsx +++ b/horreum-web/src/domain/admin/RemoveUsers.tsx @@ -12,9 +12,9 @@ import { Form, FormGroup } from "@patternfly/react-core"; -import {userName} from "../../auth"; +import { userName } from "../../utils"; import {userApi} from "../../api"; -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; export default function RemoveUsers() { diff --git a/horreum-web/src/domain/admin/Teams.tsx b/horreum-web/src/domain/admin/Teams.tsx index a09db62e2..bbb1e4fee 100644 --- a/horreum-web/src/domain/admin/Teams.tsx +++ b/horreum-web/src/domain/admin/Teams.tsx @@ -1,5 +1,4 @@ import {useContext, useEffect, useRef, useState} from "react" -import { useSelector } from "react-redux" import { Button, FormGroup, @@ -13,11 +12,12 @@ import { TabFunctionsRef } from "../../components/SavedTabs" import SplitForm from "../../components/SplitForm" import TeamMembers, { TeamMembersFunctions } from "../user/TeamMembers" import NewUserModal from "../user/NewUserModal" -import { isAdminSelector } from "../../auth" import {userApi} from "../../api" import { noop } from "../../utils" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type Team = { @@ -33,6 +33,7 @@ type TeamsProps = { export default function Teams(props: TeamsProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { isAdmin } = useContext(AuthBridgeContext) as AuthContextType; const [teams, setTeams] = useState([]) const [selected, setSelected] = useState() const [loading, setLoading] = useState(false) @@ -41,10 +42,9 @@ export default function Teams(props: TeamsProps) { const [membersModified, setMembersModified] = useState(false) const [nextTeam, setNextTeam] = useState() const [newUserModalOpen, setNewUserModalOpen] = useState(false) - const isAdmin = useSelector(isAdminSelector) const teamFuncsRef = useRef(undefined) useEffect(() => { - if (!isAdmin) { + if (!isAdmin()) { return // happens during reload } setLoading(true) diff --git a/horreum-web/src/domain/alerting/ChangeTable.tsx b/horreum-web/src/domain/alerting/ChangeTable.tsx index da0ab10b9..56a53bc86 100644 --- a/horreum-web/src/domain/alerting/ChangeTable.tsx +++ b/horreum-web/src/domain/alerting/ChangeTable.tsx @@ -19,13 +19,14 @@ import { CheckIcon } from "@patternfly/react-icons" import { NavLink } from "react-router-dom" import {alertingApi, Change, FingerprintValue, Variable} from "../../api" import { fingerprintToString, formatDateTime } from "../../utils" -import { useTester } from "../../auth" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import CustomTable from "../../components/CustomTable"; import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; const columnHelper = createColumnHelper() @@ -162,6 +163,7 @@ type ChangesProps = { export const ChangeTable = ({ varId, fingerprint, testOwner, selectedChangeId }: ChangesProps) => { const { alerting } = useContext(AppContext) as AppContextType; + const { isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; const [changes, setChanges] = useState([]) useEffect(() => { alertingApi.changes(varId, fingerprintToString(fingerprint)).then( @@ -169,7 +171,7 @@ export const ChangeTable = ({ varId, fingerprint, testOwner, selectedChangeId }: error => alerting.dispatchError(error, "DASHBOARD_FETCH", "Failed to fetch dashboard") ) }, [varId]) - const isTester = useTester(testOwner) + const isTester = isTesterFunc(testOwner) const columns: ColumnDef[] = [ columnHelper.accessor('confirmed', { header: 'Confirmed', diff --git a/horreum-web/src/domain/alerting/Changes.tsx b/horreum-web/src/domain/alerting/Changes.tsx index 5097667fe..9ae1385b8 100644 --- a/horreum-web/src/domain/alerting/Changes.tsx +++ b/horreum-web/src/domain/alerting/Changes.tsx @@ -4,7 +4,6 @@ import { SelectedTest } from "../../components/TestSelect" import LabelsSelect, { convertLabels } from "../../components/LabelsSelect" import PanelChart from "./PanelChart" import { fingerprintToString, formatDate } from "../../utils" -import { teamsSelector } from "../../auth" import { DateTime } from "luxon" import { PanelInfo, @@ -31,7 +30,7 @@ import { Spinner, } from '@patternfly/react-core'; import { useNavigate } from "react-router-dom" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import {useSelector} from "react-redux"; import { SimpleSelect } from "../../components/templates/SimpleSelect" @@ -187,9 +186,9 @@ type ChangesProps = { export default function Changes(props: ChangesProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { teams } = useContext(AuthBridgeContext) as AuthContextType; const navigate = useNavigate() const params = new URLSearchParams(location.search) - const teams = useSelector(teamsSelector) const currentTest = { id: props.testID } as SelectedTest; const paramFingerprint = params.get("fingerprint") diff --git a/horreum-web/src/domain/alerting/PanelChart.tsx b/horreum-web/src/domain/alerting/PanelChart.tsx index 2e3009db9..3f92ad4d2 100644 --- a/horreum-web/src/domain/alerting/PanelChart.tsx +++ b/horreum-web/src/domain/alerting/PanelChart.tsx @@ -19,7 +19,7 @@ import { DateTime } from "luxon" import {alertingApi, AnnotationDefinition, FingerprintValue, TimeseriesTarget} from "../../api" import { fingerprintToString } from "../../utils" import { fetchDatapoints, fetchAllAnnotations } from "./Changes" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; function tsToDate(timestamp: number) { diff --git a/horreum-web/src/domain/alerting/RecalculateModal.tsx b/horreum-web/src/domain/alerting/RecalculateModal.tsx index b513482f0..d065815f4 100644 --- a/horreum-web/src/domain/alerting/RecalculateModal.tsx +++ b/horreum-web/src/domain/alerting/RecalculateModal.tsx @@ -16,7 +16,7 @@ import { NavLink } from "react-router-dom" import {alertingApi, DatapointRecalculationStatus, DatasetInfo} from "../../api" import TimeRangeSelect, { TimeRange } from "../../components/TimeRangeSelect" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; type RecalculateModalProps = { diff --git a/horreum-web/src/domain/reports/Reports.tsx b/horreum-web/src/domain/reports/Reports.tsx index 62dc05d7a..03be82a3c 100644 --- a/horreum-web/src/domain/reports/Reports.tsx +++ b/horreum-web/src/domain/reports/Reports.tsx @@ -1,5 +1,4 @@ import {useContext, useEffect, useMemo, useState} from "react" -import { useSelector } from "react-redux" import { NavLink } from "react-router-dom" import { @@ -18,13 +17,14 @@ import { formatDateTime } from "../../utils" import {AllTableReports, reportApi, SortDirection, TableReportConfig, TableReportSummary} from "../../api" import ButtonLink from "../../components/ButtonLink" -import { useTester, teamsSelector } from "../../auth" import ListReportsModal from "./ListReportsModal" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import CustomTable from "../../components/CustomTable" import { ColumnDef, ColumnSort, createColumnHelper } from "@tanstack/react-table" +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; const columnHelper = createColumnHelper() @@ -37,6 +37,7 @@ export default function Reports(props: ReportGroup) { document.title = "Reports | Horreum" const { alerting } = useContext(AppContext) as AppContextType; + const { teams } = useContext(AuthBridgeContext) as AuthContextType; const [page, setPage] = useState(1) const [perPage, setPerPage] = useState(20) const [sortBy, setSortBy] = useState({id: 'title', desc: true}) @@ -49,7 +50,6 @@ export default function Reports(props: ReportGroup) { const [loading, setLoading] = useState(false) const [tableReportGroup, setTableReportGroup] = useState() - const teams = useSelector(teamsSelector) useEffect(() => { setLoading(true) diff --git a/horreum-web/src/domain/reports/TableReportConfigPage.tsx b/horreum-web/src/domain/reports/TableReportConfigPage.tsx index 67cce737b..7ee1ec0b9 100644 --- a/horreum-web/src/domain/reports/TableReportConfigPage.tsx +++ b/horreum-web/src/domain/reports/TableReportConfigPage.tsx @@ -43,11 +43,12 @@ import Labels from "../../components/Labels" import HelpButton from "../../components/HelpButton" import ExportButton from "../../components/ExportButton" import OptionalFunction from "../../components/OptionalFunction" -import TestSelect, { SelectedTest } from "../../components/TestSelect" +import { SelectedTest } from "../../components/TestSelect" -import { useTester } from "../../auth" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type ReportConfigComponentProps = { component: ReportComponent @@ -128,6 +129,7 @@ function ReportConfigComponent({ component, onChange, onDelete, readOnly }: Repo export default function TableReportConfigPage() { const { alerting } = useContext(AppContext) as AppContextType; + const { isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; const { testId, configId } = useParams() const id = parseInt(configId ?? "-1") const currentTestId = parseInt(testId ?? "-1") @@ -219,7 +221,7 @@ export default function TableReportConfigPage() { ], }) - const isTester = useTester(config?.test?.owner) + const isTester = isTesterFunc(config?.test?.owner) function saveButton(reportId: number | undefined, label: string, variant: "primary" | "secondary") { return ( diff --git a/horreum-web/src/domain/reports/TableReportPage.tsx b/horreum-web/src/domain/reports/TableReportPage.tsx index 53ac2b5c4..df5e1079d 100644 --- a/horreum-web/src/domain/reports/TableReportPage.tsx +++ b/horreum-web/src/domain/reports/TableReportPage.tsx @@ -18,19 +18,20 @@ import { } from "@patternfly/react-core" import { Link } from "react-router-dom" -import { useTester } from "../../auth" - -import {reportApi, TableReport, Test} from "../../api" +import {reportApi, TableReport} from "../../api" import TableReportView from "./TableReportView" import ButtonLink from "../../components/ButtonLink" import PrintButton from "../../components/PrintButton" import ReportLogModal from "./ReportLogModal" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import { SelectedTest } from "../../components/TestSelect" +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; export default function TableReportPage() { const { alerting } = useContext(AppContext) as AppContextType; + const { isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; const { id } = useParams() const idVal = parseInt(id ?? "-1") const [report, setReport] = useState() @@ -55,7 +56,7 @@ export default function TableReportPage() { } }, [idVal]) const componentRef = useRef(null) - const isTester = useTester(report?.config?.test?.owner) + const isTester = isTesterFunc(report?.config?.test?.owner) const selectedTest = report?.config?.test as SelectedTest if (loading) { return ( diff --git a/horreum-web/src/domain/runs/DatasetComparison.tsx b/horreum-web/src/domain/runs/DatasetComparison.tsx index 40cf51209..bdb57c335 100644 --- a/horreum-web/src/domain/runs/DatasetComparison.tsx +++ b/horreum-web/src/domain/runs/DatasetComparison.tsx @@ -1,5 +1,4 @@ import { ReactNode, SetStateAction, useContext, useEffect, useMemo, useRef, useState } from "react" -import { useSelector } from "react-redux" import { Breadcrumb, BreadcrumbItem, @@ -33,15 +32,16 @@ import { Link, NavLink } from "react-router-dom" import { Bar, BarChart, CartesianGrid, ResponsiveContainer, YAxis } from "recharts" import {datasetApi, fetchTest, fetchViews, Test, View} from "../../api" -import { tokenSelector } from "../../auth" import { colors } from "../../charts" import PrintButton from "../../components/PrintButton" import FragmentTabs, { FragmentTab } from "../../components/FragmentTabs" import { renderValue } from "./components" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AlertContextType, AppContextType} from "../../context/@types/appContextTypes"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type Ds = { @@ -317,9 +317,9 @@ type ViewComparisonProps = { } function ViewComparison({headers, view, datasets, alerting}: ViewComparisonProps) { + const { token } = useContext(AuthBridgeContext) as AuthContextType; const [loading, setLoading] = useState(false) const [rows, setRows] = useState([]) - const token = useSelector(tokenSelector) useEffect(() => { setLoading(true) diff --git a/horreum-web/src/domain/runs/DatasetData.tsx b/horreum-web/src/domain/runs/DatasetData.tsx index 2c1ae2b95..6cbfa1d8b 100644 --- a/horreum-web/src/domain/runs/DatasetData.tsx +++ b/horreum-web/src/domain/runs/DatasetData.tsx @@ -12,7 +12,7 @@ import { NoSchemaInDataset } from "./NoSchema" import LabelValuesModal from "./LabelValuesModal" import ExperimentModal from "./ExperimentModal" import SchemaValidations from "./SchemaValidations" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; diff --git a/horreum-web/src/domain/runs/ExperimentModal.tsx b/horreum-web/src/domain/runs/ExperimentModal.tsx index ad50b4f76..c84aecc19 100644 --- a/horreum-web/src/domain/runs/ExperimentModal.tsx +++ b/horreum-web/src/domain/runs/ExperimentModal.tsx @@ -8,7 +8,7 @@ import { NavLink } from "react-router-dom" import {experimentApi, ExperimentResult} from "../../api" import { LogLevelIcon } from "../../components/LogModal" import { interleave } from "../../utils" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; diff --git a/horreum-web/src/domain/runs/JsonPathSearchToolbar.tsx b/horreum-web/src/domain/runs/JsonPathSearchToolbar.tsx index c6405b3c7..7cb9acfd9 100644 --- a/horreum-web/src/domain/runs/JsonPathSearchToolbar.tsx +++ b/horreum-web/src/domain/runs/JsonPathSearchToolbar.tsx @@ -16,7 +16,7 @@ import { HelpIcon } from "@patternfly/react-icons" import { toString } from "../../components/Editor" import Autosuggest, { InputProps, ChangeEvent, SuggestionsFetchRequestedParams } from "react-autosuggest" import { QueryResult } from "../../api" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import { SimpleSelect } from "../../components/templates/SimpleSelect"; diff --git a/horreum-web/src/domain/runs/LabelValuesModal.tsx b/horreum-web/src/domain/runs/LabelValuesModal.tsx index d9abd83f5..ac558b50e 100644 --- a/horreum-web/src/domain/runs/LabelValuesModal.tsx +++ b/horreum-web/src/domain/runs/LabelValuesModal.tsx @@ -5,7 +5,7 @@ import { Bullseye, EmptyState, EmptyStateBody, Spinner, Tooltip, Truncate } from import {Modal} from '@patternfly/react-core/deprecated'; import {datasetApi, LabelValue} from "../../api" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import {OuterScrollContainer, Table, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; import { t_global_font_family_mono } from '@patternfly/react-tokens' diff --git a/horreum-web/src/domain/runs/MetaData.tsx b/horreum-web/src/domain/runs/MetaData.tsx index c55ff3303..bde4ceb5f 100644 --- a/horreum-web/src/domain/runs/MetaData.tsx +++ b/horreum-web/src/domain/runs/MetaData.tsx @@ -7,7 +7,7 @@ import Editor from "../../components/Editor/monaco/Editor" import { toString } from "../../components/Editor" import { NoSchemaInRun } from "./NoSchema" import SchemaValidations from "./SchemaValidations" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; diff --git a/horreum-web/src/domain/runs/Run.tsx b/horreum-web/src/domain/runs/Run.tsx index da25505c6..f61f43a36 100644 --- a/horreum-web/src/domain/runs/Run.tsx +++ b/horreum-web/src/domain/runs/Run.tsx @@ -1,13 +1,10 @@ import {useContext, useEffect, useState} from "react" import { Link, useParams } from "react-router-dom" -import { useSelector } from "react-redux" import { formatDateTime } from "../../utils" -import { teamsSelector, useTester } from "../../auth" import { Bullseye, Button, Card, CardHeader, CardBody, PageSection, Spinner, Toolbar, ToolbarContent, Breadcrumb, BreadcrumbItem } from "@patternfly/react-core" import { Table /* data-codemods */, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table" -import { TrashIcon } from "@patternfly/react-icons" import FragmentTabs, { FragmentTab } from "../../components/FragmentTabs" import OwnerAccess from "../../components/OwnerAccess" import { NavLink } from "react-router-dom" @@ -24,13 +21,16 @@ import { trash, updateRunAccess } from "../../api" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import { AppContextType} from "../../context/@types/appContextTypes"; import ConfirmDeleteModal from "../../components/ConfirmDeleteModal"; import ConfirmRestoreModal from "../../components/ConfirmRestoreModal"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; export default function Run() { const { alerting } = useContext(AppContext) as AppContextType; + const { teams, isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; const { id } = useParams() const idVal = parseInt(id ?? "-1") document.title = `Run ${idVal} | Horreum` @@ -44,8 +44,7 @@ export default function Run() { const [confirmRestoreRunModalOpen, setConfirmRestoreRunModalOpen] = useState(false) const [isTrashed, setIsTrashed] = useState(run?.trashed || false) - const teams = useSelector(teamsSelector) - const isTester = useTester(run?.owner) + const isTester = isTesterFunc(run?.owner) const retransformClick = () => { if ( run !== undefined) { diff --git a/horreum-web/src/domain/runs/RunData.tsx b/horreum-web/src/domain/runs/RunData.tsx index fb5517a4e..1a5d3be57 100644 --- a/horreum-web/src/domain/runs/RunData.tsx +++ b/horreum-web/src/domain/runs/RunData.tsx @@ -1,7 +1,4 @@ import {useContext, useEffect, useMemo, useState} from "react" -import { useSelector } from "react-redux" - -import { useTester, teamsSelector } from "../../auth" import Editor from "../../components/Editor/monaco/Editor" @@ -11,8 +8,10 @@ import ChangeSchemaModal from "./ChangeSchemaModal" import JsonPathSearchToolbar from "./JsonPathSearchToolbar" import { NoSchemaInRun } from "./NoSchema" import SchemaValidations from "./SchemaValidations" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; function findFirstValue(o: any) { if (!o || Object.keys(o).length !== 1) { @@ -40,11 +39,11 @@ type RunDataProps = { export default function RunData(props: RunDataProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { teams, isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; const [data, setData] = useState() const [editorData, setEditorData] = useState() const [changeSchemaModalOpen, setChangeSchemaModalOpen] = useState(false) - const teams = useSelector(teamsSelector) useEffect(() => { const urlParams = new URLSearchParams(window.location.search) const token = urlParams.get("token") @@ -58,7 +57,7 @@ export default function RunData(props: RunDataProps) { ) }, [ props.run.id, teams, props.updateCounter]) - const isTester = useTester(props.run.owner) + const isTester = isTesterFunc(props.run.owner) const memoizedEditor = useMemo(() => { // TODO: height 100% doesn't work return ( diff --git a/horreum-web/src/domain/runs/RunImportModal.tsx b/horreum-web/src/domain/runs/RunImportModal.tsx index 112f16c87..24b52c9e0 100644 --- a/horreum-web/src/domain/runs/RunImportModal.tsx +++ b/horreum-web/src/domain/runs/RunImportModal.tsx @@ -16,7 +16,7 @@ import AccessChoice from "../../components/AccessChoice"; import {Access as authAccess, Test} from "../../generated"; import Editor from "../../components/Editor/monaco/Editor"; import {apiCall, runApi} from "../../api"; -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; interface RunImportModalProps { diff --git a/horreum-web/src/domain/runs/RunList.tsx b/horreum-web/src/domain/runs/RunList.tsx index 2b213f59c..05f8efdef 100644 --- a/horreum-web/src/domain/runs/RunList.tsx +++ b/horreum-web/src/domain/runs/RunList.tsx @@ -1,6 +1,5 @@ import {useState, useMemo, useEffect, useContext} from "react" import { useParams } from "react-router-dom" -import { useSelector } from "react-redux" import { Button, Checkbox, @@ -16,7 +15,7 @@ import { NavLink } from "react-router-dom" import { Duration } from "luxon" import { toEpochMillis, noop } from "../../utils" -import {isAuthenticatedSelector, teamsSelector, teamToName} from "../../auth" +import { teamToName } from "../../utils" import { fetchTest } from "../../api" @@ -25,19 +24,21 @@ import { NoSchemaInRun } from "./NoSchema" import { Description, ExecutionTime, Menu } from "./components" import SchemaList from "./SchemaList" import AccessIcon from "../../components/AccessIcon" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import {RunImportModal} from "./RunImportModal"; import CustomTable from "../../components/CustomTable" import { ColumnDef, ColumnSort, createColumnHelper } from "@tanstack/react-table" +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; const columnHelper = createColumnHelper() export default function RunList() { const { alerting } = useContext(AppContext) as AppContextType; + const { isAuthenticated, teams } = useContext(AuthBridgeContext) as AuthContextType; const { testId } = useParams() const testIdInt = parseInt(testId ?? "-1") - const isAuthenticated = useSelector(isAuthenticatedSelector) const [test, setTest] = useState(undefined) const [selectedRows, setSelectedRows] = useState>({}) @@ -49,7 +50,6 @@ export default function RunList() { const [showTrashed, setShowTrashed] = useState(false) const [runs, setRuns] = useState([]) const [runCount, setRunCount] =useState(0) - const teams = useSelector(teamsSelector) const [isLoading, setIsLoading] = useState(false) const [showNewRunModal, setShowNewRunModal] = useState(false) diff --git a/horreum-web/src/domain/runs/TestDatasets.tsx b/horreum-web/src/domain/runs/TestDatasets.tsx index 27f7a4f0c..45077d6f8 100644 --- a/horreum-web/src/domain/runs/TestDatasets.tsx +++ b/horreum-web/src/domain/runs/TestDatasets.tsx @@ -6,7 +6,6 @@ import { import {useCallback, useContext, useEffect, useMemo, useState} from "react" import { useParams } from "react-router-dom" -import { useSelector } from "react-redux" import { Button, Flex, @@ -22,7 +21,7 @@ import { NavLink } from "react-router-dom" import { Duration } from "luxon" import { toEpochMillis, fingerprintToString } from "../../utils" -import { teamsSelector, teamToName, tokenSelector } from "../../auth" +import { teamToName } from "../../utils" import { DatasetSummary, @@ -39,11 +38,13 @@ import { Description, ExecutionTime, renderCell } from "./components" import ButtonLink from "../../components/ButtonLink" import ViewSelect from "../../components/ViewSelect" import AccessIcon from "../../components/AccessIcon" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import CustomTable from "../../components/CustomTable" import { ColumnDef, ColumnSort, createColumnHelper } from '@tanstack/react-table'; import LabelFilter from "../../components/LabelFilter/LabelFilter"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; const columnHelper = createColumnHelper() @@ -77,6 +78,7 @@ const staticColumns: ColumnDef[] = [ export default function TestDatasets() { const { alerting } = useContext(AppContext) as AppContextType; + const { token, teams } = useContext(AuthBridgeContext) as AuthContextType; const { testId } = useParams(); const testIdInt = parseInt(testId ?? "-1") const [test, setTest] = useState(undefined) @@ -89,8 +91,6 @@ export default function TestDatasets() { const [loading, setLoading] = useState(false) const [datasets, setDatasets] = useState() const [comparedDatasets, setComparedDatasets] = useState([]) - const teams = useSelector(teamsSelector) - const token = useSelector(tokenSelector) const [views, setViews] = useState([]) useEffect(() => { diff --git a/horreum-web/src/domain/runs/components.tsx b/horreum-web/src/domain/runs/components.tsx index 2ffe09819..fcb70ec98 100644 --- a/horreum-web/src/domain/runs/components.tsx +++ b/horreum-web/src/domain/runs/components.tsx @@ -16,11 +16,11 @@ import ActionMenu, { useDelete, } from "../../components/ActionMenu" import { formatDateTime, toEpochMillis } from "../../utils" -import { useTester } from "../../auth" import {Access, recalculateDatasets, RunSummary, trash, updateDescription, updateRunAccess} from "../../api" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; -import { Cell } from "@tanstack/react-table"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; export function Description(description: string) { const truncated = ( @@ -161,6 +161,7 @@ function useUpdateDescription(run: RunSummary): MenuItem { export function Menu(run: RunSummary, refreshCallback: () => void, clearSelected: () => void) { const { alerting } = useContext(AppContext) as AppContextType; + const { isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; const changeAccess = useChangeAccess({ onAccessUpdate: (id, owner, access) => updateRunAccess(id, run.testid, owner, access, alerting).then(refreshCallback), @@ -174,7 +175,7 @@ export function Menu(run: RunSummary, refreshCallback: () => void, clearSelected const menuItems: MenuItem[] = [changeAccess, recalculate] menuItems.push(run.trashed ? restore : del) - const isTester = useTester(run.owner) + const isTester = isTesterFunc(run.owner) const updateDescription = useUpdateDescription(run) if (isTester) { menuItems.push(updateDescription) diff --git a/horreum-web/src/domain/schemas/FindUsagesModal.tsx b/horreum-web/src/domain/schemas/FindUsagesModal.tsx index d486e3717..2ff3c4842 100644 --- a/horreum-web/src/domain/schemas/FindUsagesModal.tsx +++ b/horreum-web/src/domain/schemas/FindUsagesModal.tsx @@ -25,7 +25,7 @@ import { } from "../../api" import ButtonLink from "../../components/ButtonLink" import NameUri from "../../components/NameUri" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; diff --git a/horreum-web/src/domain/schemas/Labels.tsx b/horreum-web/src/domain/schemas/Labels.tsx index c701d0257..68668f925 100644 --- a/horreum-web/src/domain/schemas/Labels.tsx +++ b/horreum-web/src/domain/schemas/Labels.tsx @@ -1,6 +1,4 @@ import {useState, useEffect, useContext} from "react" -import { useSelector } from "react-redux" - import { Button, Checkbox, @@ -12,8 +10,6 @@ import { Button, HelperTextItem, FormHelperText, TextInput } from "@patternfly/react-core" - -import { defaultTeamSelector, useTester } from "../../auth" import { TabFunctionsRef } from "../../components/SavedTabs" import FindUsagesModal from "./FindUsagesModal" @@ -25,8 +21,10 @@ import SplitForm from "../../components/SplitForm" import TestLabelModal from "./TestLabelModal" import { Label, schemaApi, Access } from "../../api" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; const LABEL_FUNCTION_HELP = ( @@ -53,6 +51,7 @@ type LabelsProps = { export default function Labels({ schemaId, schemaUri, funcsRef }: LabelsProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { defaultTeam, isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; const [loading, setLoading] = useState(false) const [labels, setLabels] = useState([]) const [selected, setSelected] = useState() @@ -60,9 +59,8 @@ export default function Labels({ schemaId, schemaUri, funcsRef }: LabelsProps) { const [deleted, setDeleted] = useState([]) const [findUsagesLabel, setFindUsagesLabel] = useState() const [testLabelModalOpen, setTestLabelModalOpen] = useState(false) - const isTester = useTester() - const isTesterForLabel = useTester(selected?.owner || "__no_owner__") - const defaultTeam = useSelector(defaultTeamSelector) + const isTester = isTesterFunc() + const isTesterForLabel = isTesterFunc(selected?.owner || "__no_owner__") funcsRef.current = { save: () =>{ const labelsToUpdate = labels.filter(l => l.modified && l.id > 0); diff --git a/horreum-web/src/domain/schemas/Schema.tsx b/horreum-web/src/domain/schemas/Schema.tsx index 194fc74f1..4a4595d40 100644 --- a/horreum-web/src/domain/schemas/Schema.tsx +++ b/horreum-web/src/domain/schemas/Schema.tsx @@ -1,6 +1,5 @@ import {useEffect, useState, useRef, useContext} from "react" import { useNavigate, useParams } from "react-router-dom" -import { useSelector } from "react-redux" import { Alert, @@ -26,7 +25,7 @@ import { ImportIcon } from "@patternfly/react-icons" import { Link } from "react-router-dom" import jsonpath from "jsonpath" -import { defaultTeamSelector, teamToName, useTester } from "../../auth" +import { teamToName } from "../../utils" import { noop } from "../../utils" import { toString } from "../../components/Editor" @@ -37,11 +36,13 @@ import SavedTabs, { SavedTab, TabFunctions, modifiedFunc, resetFunc, saveFunc } import TeamSelect from "../../components/TeamSelect" import Transformers from "./Transformers" import Labels from "./Labels" -import {Access, getSchema, Schema as SchemaDef, schemaApi, Banner as BannerData} from "../../api" -import {AppContext} from "../../context/appContext"; +import {Access, getSchema, Schema as SchemaDef, schemaApi} from "../../api" +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import { TimeoutBanner, TimeoutBannerProps } from "../../Banner" import ExportButton from "../../components/ExportButton"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type GeneralProps = { @@ -53,8 +54,8 @@ type GeneralProps = { const SUPPORTED_SCHEMES = ["uri:", "urn:", "http:", "https:", "ftp:", "file:", "jar:"] function General(props: GeneralProps) { - const defaultTeam = useSelector(defaultTeamSelector) - const isTester = useTester(props.schema?.owner) + const { defaultTeam, isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; + const isTester = isTesterFunc(props.schema?.owner) const [importFailed, setImportFailed] = useState(false) const schema: SchemaDef = props.schema || { @@ -203,6 +204,7 @@ function General(props: GeneralProps) { export default function Schema() { const navigate = useNavigate() const { alerting } = useContext(AppContext) as AppContextType; + const { isTester } = useContext(AuthBridgeContext) as AuthContextType; const { schemaId } = useParams() const [schemaIdVal, setSchemaIdVal] = useState(schemaId === "_new" ? -1 : Number.parseInt(schemaId ?? "-1")) const [schema, setSchema] = useState(undefined) @@ -213,8 +215,7 @@ export default function Schema() { const [showMessageBanner, setShowMessageBanner] = useState(false) // any tester can save to add new labels/transformers - const isTester = useTester() - const isTesterForSchema = useTester(schema?.owner) + const isTesterForSchema = isTester(schema?.owner) useEffect(() => { if (schemaIdVal >= 0) { @@ -307,7 +308,7 @@ export default function Schema() { afterReset={() => { setModified(false) }} - canSave={isTester} + canSave={isTester()} > Promise.resolve()} - isHidden={!isTester} + isHidden={!isTester()} isModified={() => false} > diff --git a/horreum-web/src/domain/schemas/SchemaLink.tsx b/horreum-web/src/domain/schemas/SchemaLink.tsx index 65e9c4a91..f6fe05a8f 100644 --- a/horreum-web/src/domain/schemas/SchemaLink.tsx +++ b/horreum-web/src/domain/schemas/SchemaLink.tsx @@ -1,7 +1,7 @@ import { schemaApi } from "../../api" import IndirectLink from "../../components/IndirectLink" import {useContext} from "react"; -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; type SchemaLinkProps = { diff --git a/horreum-web/src/domain/schemas/SchemaList.tsx b/horreum-web/src/domain/schemas/SchemaList.tsx index f3a7cd754..9a341424f 100644 --- a/horreum-web/src/domain/schemas/SchemaList.tsx +++ b/horreum-web/src/domain/schemas/SchemaList.tsx @@ -8,7 +8,7 @@ import { } from "@patternfly/react-core" import {NavLink, useNavigate} from "react-router-dom" -import {useTester, teamsSelector, teamToName} from "../../auth" +import { teamToName } from "../../utils" import {noop} from "../../utils" import ActionMenu, {useChangeAccess, useDelete} from "../../components/ActionMenu" @@ -16,13 +16,14 @@ import ButtonLink from "../../components/ButtonLink" import {Access, SortDirection, Schema, schemaApi, SchemaExport} from "../../api" import TeamSelect, {ONLY_MY_OWN, Team, createTeam} from "../../components/TeamSelect"; import AccessIcon from "../../components/AccessIcon" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; -import {useSelector} from "react-redux"; import ImportButton from "../../components/ImportButton"; import CustomTable from "../../components/CustomTable" import FilterSearchInput from "../../components/FilterSearchInput"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table" +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; const columnHelper = createColumnHelper(); @@ -31,7 +32,7 @@ export default function SchemaList() { const params = new URLSearchParams(location.search) const navigate = useNavigate() const {alerting} = useContext(AppContext) as AppContextType; - const teams = useSelector(teamsSelector) + const { teams, isTester } = useContext(AuthBridgeContext) as AuthContextType; const [page, setPage] = useState(1) const [perPage, setPerPage] = useState(20) @@ -45,8 +46,6 @@ export default function SchemaList() { const [rolesFilter, setRolesFilter] = useState(rolesFilterFromQuery !== null ? createTeam(rolesFilterFromQuery) : ONLY_MY_OWN) const [nameFilter, setNameFilter] = useState("") - const isTester = useTester() - const removeSchema = (id: number) => { if (schemaCount > 0) { setSchemas(schemas?.filter(schema => schema.id !== id) || []) @@ -136,7 +135,7 @@ export default function SchemaList() { - {isTester && ( + {isTester() && ( setNameFilter("")} /> - {isTester && ( + {isTester() && ( ([]) const [selected, setSelected] = useState() const [deleted, setDeleted] = useState([]) - const defaultTeam = useSelector(defaultTeamSelector) - const isTester = useTester() - const isTesterForTransformer = useTester(selected?.owner) + const isTester = isTesterFunc() + const isTesterForTransformer = isTesterFunc(selected?.owner) const update = (update: Partial) => { if (!selected) { return diff --git a/horreum-web/src/domain/schemas/TryJsonPathModal.tsx b/horreum-web/src/domain/schemas/TryJsonPathModal.tsx index 75308d29a..adb628bbb 100644 --- a/horreum-web/src/domain/schemas/TryJsonPathModal.tsx +++ b/horreum-web/src/domain/schemas/TryJsonPathModal.tsx @@ -7,7 +7,7 @@ import { NavLink } from "react-router-dom" import JsonPathDocsLink from "../../components/JsonPathDocsLink" import Editor from "../../components/Editor/monaco/Editor" import {datasetApi, DatasetSummary, QueryResult, runApi, RunSummary, SortDirection, sqlApi} from "../../api" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import {OuterScrollContainer, Table, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; diff --git a/horreum-web/src/domain/tests/ActionsUI.tsx b/horreum-web/src/domain/tests/ActionsUI.tsx index ae976e738..c782fb3ac 100644 --- a/horreum-web/src/domain/tests/ActionsUI.tsx +++ b/horreum-web/src/domain/tests/ActionsUI.tsx @@ -11,16 +11,16 @@ import { DataListCell, Title, } from "@patternfly/react-core" - -import { useTester } from "../../auth" import {Action, getTestActions, updateOrCreateActions, HttpActionConfig, ActionConfig} from "../../api" import { TabFunctionsRef } from "../../components/SavedTabs" import { testEventTypes } from "../actions/reducers" import ActionComponentForm from "../actions/ActionComponentForm" import ActionLogModal from "./ActionLogModal" import { useNavigate } from "react-router-dom" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type ActionsProps = { @@ -32,9 +32,10 @@ type ActionsProps = { export default function ActionsUI({ testId, testOwner, funcsRef, onModified }: ActionsProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; const [actions, setActions] = useState([]) const [logModalOpen, setLogModalOpen] = useState(false) - const isTester = useTester(testOwner) + const isTester = isTesterFunc(testOwner) const hasDuplicates = new Set(actions.map(h => h.event + "_" + (h.config as HttpActionConfig).url)).size !== actions.length useEffect(() => { diff --git a/horreum-web/src/domain/tests/AllTests.tsx b/horreum-web/src/domain/tests/AllTests.tsx index b06551bc3..a97a0785c 100644 --- a/horreum-web/src/domain/tests/AllTests.tsx +++ b/horreum-web/src/domain/tests/AllTests.tsx @@ -1,7 +1,5 @@ import React, {useContext, useEffect, useMemo, useState} from "react" -import {useSelector} from "react-redux" - import { Breadcrumb, BreadcrumbHeading, @@ -30,7 +28,7 @@ import FolderSelect from "../../components/FolderSelect" import ConfirmTestDeleteModal from "./ConfirmTestDeleteModal" import RecalculateDatasetsModal from "./RecalculateDatasetsModal" -import {isAuthenticatedSelector, teamsSelector, teamToName, userProfileSelector, useTester} from "../../auth" +import { teamToName } from "../../utils" import {noop} from "../../utils" import { Access, @@ -46,13 +44,15 @@ import { fetchFolders, testApi, TestExport } from "../../api" import AccessIcon from "../../components/AccessIcon" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import FoldersDropDown from "../../components/FoldersDropdown"; import ImportButton from "../../components/ImportButton"; import CustomTable from "../../components/CustomTable"; import FilterSearchInput from "../../components/FilterSearchInput"; import { ColumnDef, ColumnSort, createColumnHelper } from "@tanstack/react-table"; +import {AuthContextType} from "../../context/@types/authContextTypes"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; type WatchDropdownProps = { id: number @@ -62,10 +62,16 @@ type WatchDropdownProps = { const DEFAULT_FOLDER = ""; const WatchDropdown = ({ id, watching }: WatchDropdownProps) => { + const { username, teams } = useContext(AuthBridgeContext) as AuthContextType; + const { alerting } = useContext(AppContext) as AppContextType; const [open, setOpen] = useState(false) - const teams = useSelector(teamsSelector) - const profile = useSelector(userProfileSelector) + const [self, setSelf] = useState("__self") + + useEffect(() => { + setSelf(username || "__self") + }, [username]); + const onSelect = () => { setOpen(false); }; @@ -74,9 +80,9 @@ const WatchDropdown = ({ id, watching }: WatchDropdownProps) => { return } const personalItems = [] - const self = profile?.username || "__self" + const isOptOut = watching.some(u => u.startsWith("!")) - if (watching.some(u => u === profile?.username)) { + if (watching.some(u => u === username)) { personalItems.push( removeUserOrTeam(id, self, alerting).catch(noop)}> Stop watching personally @@ -284,6 +290,7 @@ const columnHelper = createColumnHelper() export default function AllTests() { const { alerting } = useContext(AppContext) as AppContextType; + const { isAuthenticated, teams, isTester } = useContext(AuthBridgeContext) as AuthContextType; const navigate = useNavigate() const location = useLocation() const params = new URLSearchParams(location.search) @@ -353,8 +360,6 @@ export default function AllTests() { ) const [allTests, setTests] = useState([]) - const teams = useSelector(teamsSelector) - const isAuthenticated = useSelector(isAuthenticatedSelector) const rolesFilterFromQuery = params.get("filter") const [rolesFilter, setRolesFilter] = useState(rolesFilterFromQuery !== null ? createTeam(rolesFilterFromQuery) : ONLY_MY_OWN) @@ -404,8 +409,6 @@ export default function AllTests() { navigate(location.pathname + query) }, [folder, rolesFilter]) - const isTester = useTester() - return ( @@ -446,7 +449,7 @@ export default function AllTests() { /> - {isTester && ( + {isTester() && ( { let errMsg = undefined diff --git a/horreum-web/src/domain/tests/ConfirmTestDeleteModal.tsx b/horreum-web/src/domain/tests/ConfirmTestDeleteModal.tsx index 7f9c8e6f1..5b1661346 100644 --- a/horreum-web/src/domain/tests/ConfirmTestDeleteModal.tsx +++ b/horreum-web/src/domain/tests/ConfirmTestDeleteModal.tsx @@ -2,7 +2,7 @@ import {useState, useEffect, useContext} from "react" import { runApi, RunCount } from "../../api" import {Bullseye, Button, ButtonVariant, TextInput, Spinner} from '@patternfly/react-core'; import {Modal} from '@patternfly/react-core/deprecated'; -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; type ConfirmTestDeleteModalProps = { diff --git a/horreum-web/src/domain/tests/Experiments.tsx b/horreum-web/src/domain/tests/Experiments.tsx index e23262558..124f25f3c 100644 --- a/horreum-web/src/domain/tests/Experiments.tsx +++ b/horreum-web/src/domain/tests/Experiments.tsx @@ -17,7 +17,6 @@ import { } from '@patternfly/react-core'; import {alertingApi, ConditionConfig, experimentApi, Test, Variable} from "../../api" -import { useTester } from "../../auth" import HelpButton from "../../components/HelpButton" import Labels from "../../components/Labels" import { TabFunctionsRef } from "../../components/SavedTabs" @@ -25,7 +24,7 @@ import SplitForm from "../../components/SplitForm" import { ExperimentProfile } from "../../generated/models/ExperimentProfile" import OptionalFunction from "../../components/OptionalFunction" import ConditionComponent from "../../components/ConditionComponent" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import { SimpleSelect } from "../../components/templates/SimpleSelect"; @@ -37,7 +36,9 @@ type ExperimentsProps = { export default function Experiments(props: ExperimentsProps) { const { alerting } = useContext(AppContext) as AppContextType; - const isTester = useTester(props.test?.owner) + const { isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; + + const isTester = isTesterFunc(props.test?.owner) const [profiles, setProfiles] = useState([]) const [selected, setSelected] = useState() const [deleted, setDeleted] = useState([]) diff --git a/horreum-web/src/domain/tests/FoldersTree.tsx b/horreum-web/src/domain/tests/FoldersTree.tsx index 16f6c2bc6..8d228b10e 100644 --- a/horreum-web/src/domain/tests/FoldersTree.tsx +++ b/horreum-web/src/domain/tests/FoldersTree.tsx @@ -1,13 +1,13 @@ import {useContext, useEffect, useState} from "react" -import { useSelector } from "react-redux" import { TreeView, TreeViewDataItem } from "@patternfly/react-core" import { FolderIcon, FolderOpenIcon } from "@patternfly/react-icons" -import { teamsSelector } from "../../auth" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import {fetchFolders} from "../../api"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type FoldersTreeProps = { folder: string @@ -16,8 +16,8 @@ type FoldersTreeProps = { export default function FoldersTree(props: FoldersTreeProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { teams } = useContext(AuthBridgeContext) as AuthContextType; const [folders, setFolders] = useState([]) - const teams = useSelector(teamsSelector) useEffect(() => { fetchFolders(alerting).then(setFolders) }, [teams]) diff --git a/horreum-web/src/domain/tests/MissingDataNotifications.tsx b/horreum-web/src/domain/tests/MissingDataNotifications.tsx index 210094651..a41d533a4 100644 --- a/horreum-web/src/domain/tests/MissingDataNotifications.tsx +++ b/horreum-web/src/domain/tests/MissingDataNotifications.tsx @@ -18,10 +18,11 @@ import SplitForm from "../../components/SplitForm" import { TabFunctionsRef } from "../../components/SavedTabs" import {alertingApi, MissingDataRule, Test} from "../../api" -import { useTester } from "../../auth" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import {useLocation} from "react-router-dom"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type MissingDataRuleExtended = MissingDataRule & { @@ -47,6 +48,7 @@ function asWarning(text: string) { export default function MissingDataNotifications(props: MissingDataNotificationsProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; const [rules, setRules] = useState() const [deleted, setDeleted] = useState([]) const [selectedRule, setSelectedRule] = useState() @@ -82,7 +84,7 @@ export default function MissingDataNotifications(props: MissingDataNotifications ) }, [props.test, resetCounter]) - const isTester = useTester(props.test?.owner) + const isTester = isTesterFunc(props.test?.owner) if (!props.test || !rules) { return ( diff --git a/horreum-web/src/domain/tests/RecalculateDatasetsModal.tsx b/horreum-web/src/domain/tests/RecalculateDatasetsModal.tsx index d0979e2d0..172141fcb 100644 --- a/horreum-web/src/domain/tests/RecalculateDatasetsModal.tsx +++ b/horreum-web/src/domain/tests/RecalculateDatasetsModal.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState, useRef, useContext } from "react" import {Bullseye, Button, Progress, Spinner} from '@patternfly/react-core'; import {Modal} from '@patternfly/react-core/deprecated'; import { fetchTest, RecalculationStatus, testApi, TestStorage } from "../../api" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; diff --git a/horreum-web/src/domain/tests/Subscriptions.tsx b/horreum-web/src/domain/tests/Subscriptions.tsx index aa6c3eb96..b548707b4 100644 --- a/horreum-web/src/domain/tests/Subscriptions.tsx +++ b/horreum-web/src/domain/tests/Subscriptions.tsx @@ -3,11 +3,13 @@ import { noop } from "../../utils" import {Divider} from '@patternfly/react-core'; import {DualListSelector} from '@patternfly/react-core/deprecated'; import {getSubscription, updateSubscription, userApi, UserData} from "../../api" -import { teamToName, useTester } from "../../auth" +import { teamToName } from "../../utils" import { TabFunctionsRef } from "../../components/SavedTabs" import UserSearch from "../../components/UserSearch" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type SubscriptionsProps = { testId: number @@ -37,7 +39,9 @@ function teamElement(team: string): ReactElement { export default function Subscriptions(props: SubscriptionsProps) { const { alerting } = useContext(AppContext) as AppContextType; - const isTester = useTester(props.testOwner) + const { isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; + + const isTester = isTesterFunc(props.testOwner) const [availableUsers, setAvailableUsers] = useState[]>([]) const [watchingUsers, setWatchingUsers] = useState[]>([]) const [optoutUsers, setOptoutUsers] = useState[]>([]) diff --git a/horreum-web/src/domain/tests/Test.tsx b/horreum-web/src/domain/tests/Test.tsx index e440f76b2..67b65bd31 100644 --- a/horreum-web/src/domain/tests/Test.tsx +++ b/horreum-web/src/domain/tests/Test.tsx @@ -1,8 +1,6 @@ import { useState, useEffect, useRef, useContext } from "react" import { useNavigate, useParams } from "react-router-dom" -import { useSelector } from "react-redux" - import { Breadcrumb, BreadcrumbItem, @@ -19,7 +17,6 @@ import { Link } from "react-router-dom" import SavedTabs, { SavedTab, TabFunctions, saveFunc, resetFunc, modifiedFunc } from "../../components/SavedTabs" -import { useTester, teamsSelector } from "../../auth" import TestSettings from "./TestSettings" import Views from "./Views" import ChangeDetectionForm from "./ChangeDetectionForm" @@ -29,7 +26,7 @@ import Subscriptions from "./Subscriptions" import Transformers from "./Transformers" import MissingDataNotifications from "./MissingDataNotifications" import { fetchTest, fetchViews, Test, testApi, View } from "../../api"; -import { AppContext } from "../../context/appContext"; +import { AppContext } from "../../context/AppContext"; import { AppContextType } from "../../context/@types/appContextTypes"; import TestDatasets from "../runs/TestDatasets"; @@ -37,8 +34,11 @@ import Changes from "../alerting/Changes"; import Reports from "../reports/Reports"; import RunList from "../runs/RunList"; import ExportButton from "../../components/ExportButton"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; export default function TestView() { + const { teams, isTester } = useContext(AuthBridgeContext) as AuthContextType; const navigate = useNavigate() const {testId} = useParams() const [testIdVal, setTestIdVal] = useState(testId === "_new" ? 0 : parseInt(testId ?? "-1")) @@ -55,9 +55,6 @@ export default function TestView() { const transformersFuncsRef = useRef(undefined) const [loaded, setLoaded] = useState(false) - //replace redux - const teams = useSelector(teamsSelector) - const refetchTest = () => { setLoaded(false) return fetchTest(testIdVal, alerting) @@ -84,9 +81,6 @@ export default function TestView() { } }, [testIdVal]) - //TODO:: replace redux - const isTester = useTester(test ? test.owner : undefined) - return ( @@ -118,7 +112,7 @@ export default function TestView() { alerting.dispatchInfo("SAVE", "Saved!", "Test was successfully updated!", 3000) }} afterReset={() => setModified(false)} - canSave={isTester} + canSave={isTester()} > Promise.resolve()} - isHidden={testIdVal <= 0 || !isTester} + isHidden={testIdVal <= 0 || !isTester()} isModified={() => false} >
diff --git a/horreum-web/src/domain/tests/TestSettings.tsx b/horreum-web/src/domain/tests/TestSettings.tsx index 62f04e3cf..cf7edcb0c 100644 --- a/horreum-web/src/domain/tests/TestSettings.tsx +++ b/horreum-web/src/domain/tests/TestSettings.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect, useContext } from "react" -import { useSelector } from "react-redux" import { Form, @@ -19,12 +18,14 @@ import FolderSelect from "../../components/FolderSelect" import { TabFunctionsRef } from "../../components/SavedTabs" import { Test, Access, addTest, configApi, Datastore, apiCall, updateTest } from "../../api" -import { useTester, defaultTeamSelector, teamToName } from "../../auth" -import { AppContext } from "../../context/appContext"; +import { teamToName } from "../../utils" +import { AppContext } from "../../context/AppContext"; import { AppContextType } from "../../context/@types/appContextTypes"; import TeamSelect from "../../components/TeamSelect" import AccessChoice from "../../components/AccessChoice" import AccessIcon from "../../components/AccessIcon" +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type GeneralProps = { test?: Test @@ -50,7 +51,8 @@ const isUrlOrBlank = (input:string|undefined):boolean=>{ export default function TestSettings({ test, onTestIdChange, onModified, funcsRef }: GeneralProps) { const { alerting } = useContext(AppContext) as AppContextType; - const defaultRole = useSelector(defaultTeamSelector) + const { defaultTeam, isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; + // general settings const [name, setName] = useState("") const [folder, setFolder] = useState("") @@ -60,7 +62,7 @@ export default function TestSettings({ test, onTestIdChange, onModified, funcsRe const [notificationsEnabled, setNotificationsEnabled] = useState(true) const [datastores, setDatastores] = useState([]) // permissions - const [owner, setOwner] = useState(test?.owner || defaultRole || "") + const [owner, setOwner] = useState(test?.owner || defaultTeam || "") const [access, setAccess] = useState(test?.access || Access.Public) useEffect( () => { @@ -75,7 +77,7 @@ export default function TestSettings({ test, onTestIdChange, onModified, funcsRe setDescription(t?.description || "") setCompareUrl(t?.compareUrl?.toString() || undefined) setNotificationsEnabled(!t || t.notificationsEnabled) - setOwner(t?.owner || defaultRole || "") + setOwner(t?.owner || defaultTeam || "") setAccess(t?.access || Access.Public) } const handleOptionChange = (_ : React.FormEvent, value: string) => { @@ -101,7 +103,7 @@ export default function TestSettings({ test, onTestIdChange, onModified, funcsRe notificationsEnabled, fingerprintLabels: test?.fingerprintLabels || [], fingerprintFilter: test?.fingerprintFilter, - owner: owner || defaultRole || "__test_created_without_a_role__", + owner: owner || defaultTeam || "__test_created_without_a_role__", access: access, transformers: test?.transformers || [], } @@ -115,7 +117,7 @@ export default function TestSettings({ test, onTestIdChange, onModified, funcsRe reset: () => updateState(test), } - const isTester = useTester(test?.owner) + const isTester = isTesterFunc(test?.owner) const isUrlValid = isUrlOrBlank(compareUrl) return ( <> diff --git a/horreum-web/src/domain/tests/Transformers.tsx b/horreum-web/src/domain/tests/Transformers.tsx index cc2a0df55..7b96c7941 100644 --- a/horreum-web/src/domain/tests/Transformers.tsx +++ b/horreum-web/src/domain/tests/Transformers.tsx @@ -14,14 +14,14 @@ import { Tooltip } from '@patternfly/react-core'; import {DualListSelector} from '@patternfly/react-core/deprecated'; - -import { useTester } from "../../auth" import { TabFunctionsRef } from "../../components/SavedTabs" import {schemaApi, Transformer, Access, TransformerInfo, updateTransformers} from "../../api" import TransformationLogModal from "./TransformationLogModal" import RecalculateDatasetsModal from "./RecalculateDatasetsModal" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type TransformersProps = { @@ -108,6 +108,7 @@ function excludeSelected(tree: SchemaItem[], excluded: Transformer[]) { export default function Transformers(props: TransformersProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; const [counter, setCounter] = useState(0) const [loading, setLoading] = useState(false) const [originalOptions, setOriginalOptions] = useState([]) @@ -115,7 +116,7 @@ export default function Transformers(props: TransformersProps) { const [chosen, setChosen] = useState([]) const [logModalOpen, setLogModalOpen] = useState(false) const [recalculateModalOpen, setRecalculateModalOpen] = useState(false) - const isTester = useTester(props.owner) + const isTester = isTesterFunc(props.owner) useEffect(() => { setLoading(true) diff --git a/horreum-web/src/domain/tests/Views.tsx b/horreum-web/src/domain/tests/Views.tsx index 862f25e58..610febf2c 100644 --- a/horreum-web/src/domain/tests/Views.tsx +++ b/horreum-web/src/domain/tests/Views.tsx @@ -12,16 +12,16 @@ import { TextInput, } from "@patternfly/react-core" -import { useTester } from "../../auth" - import Labels from "../../components/Labels" import OptionalFunction from "../../components/OptionalFunction" import {deleteView, updateView, View, ViewComponent} from "../../api" import { TabFunctionsRef } from "../../components/SavedTabs" import SplitForm from "../../components/SplitForm" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import {useLocation, useNavigate} from "react-router-dom"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; function swap(array: any[], i1: number, i2: number) { const temp = array[i1] @@ -108,7 +108,8 @@ function deepCopy(views: View[]): ViewExtended[] { export default function Views({ testId, testOwner, funcsRef, onModified, ...props }: ViewsProps) { const { alerting } = useContext(AppContext) as AppContextType; - const isTester = useTester(testOwner) + const { isTester: isTesterFunc } = useContext(AuthBridgeContext) as AuthContextType; + const isTester = isTesterFunc(testOwner) const [views, setViews] = useState([]) const [deleted, setDeleted] = useState([]) const [selectedView, setSelectedView] = useState() diff --git a/horreum-web/src/domain/user/ApiKeys.tsx b/horreum-web/src/domain/user/ApiKeys.tsx index d17a59427..5bfcbab29 100644 --- a/horreum-web/src/domain/user/ApiKeys.tsx +++ b/horreum-web/src/domain/user/ApiKeys.tsx @@ -13,7 +13,7 @@ import { import {Modal} from '@patternfly/react-core/deprecated'; import {ApiKeyResponse, userApi} from "../../api"; -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; import {ActionsColumn, Table, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; diff --git a/horreum-web/src/domain/user/ManagedTeams.tsx b/horreum-web/src/domain/user/ManagedTeams.tsx index f260f547a..71b50a2e8 100644 --- a/horreum-web/src/domain/user/ManagedTeams.tsx +++ b/horreum-web/src/domain/user/ManagedTeams.tsx @@ -1,14 +1,14 @@ -import { useState, useRef } from "react" -import { useSelector } from "react-redux" +import {useState, useRef, useContext} from "react" import {Button, Form, FormGroup} from '@patternfly/react-core'; import {Modal} from '@patternfly/react-core/deprecated'; import { TabFunctionsRef } from "../../components/SavedTabs" import TeamSelect, { createTeam, Team } from "../../components/TeamSelect" -import { defaultTeamSelector, managedTeamsSelector } from "../../auth" import TeamMembers, { TeamMembersFunctions } from "./TeamMembers" import NewUserModal from "./NewUserModal" import { noop } from "../../utils" +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type ManagedTeamsProps = { funcs: TabFunctionsRef @@ -16,12 +16,9 @@ type ManagedTeamsProps = { } export default function ManagedTeams(props: ManagedTeamsProps) { - let defaultTeam = useSelector(defaultTeamSelector) - const managedTeams = useSelector(managedTeamsSelector) - if (!defaultTeam || !managedTeams.includes(defaultTeam)) { - defaultTeam = managedTeams.length > 0 ? managedTeams[0] : undefined - } - const [team, setTeam] = useState(createTeam(defaultTeam)) + const { managedTeams, defaultTeam } = useContext(AuthBridgeContext) as AuthContextType; + const localDefaultTeam = (!defaultTeam || !managedTeams.includes(defaultTeam)) ? (managedTeams.length > 0 ? managedTeams[0] : undefined) : defaultTeam + const [team, setTeam] = useState(createTeam(localDefaultTeam)) const [nextTeam, setNextTeam] = useState() const [createNewUser, setCreateNewUser] = useState(false) const [modified, setModified] = useState(false) @@ -46,7 +43,7 @@ export default function ManagedTeams(props: ManagedTeamsProps) { (modified && setNextTeam(anotherTeam)) || setTeam(anotherTeam)} /> diff --git a/horreum-web/src/domain/user/NewUserModal.tsx b/horreum-web/src/domain/user/NewUserModal.tsx index f3b085eaf..1f3722240 100644 --- a/horreum-web/src/domain/user/NewUserModal.tsx +++ b/horreum-web/src/domain/user/NewUserModal.tsx @@ -15,10 +15,10 @@ import { import {Modal} from '@patternfly/react-core/deprecated'; import {userApi, UserData} from "../../api" import { getRoles } from "./TeamMembers" -import {AppContext} from "../../context/appContext"; +import {AppContext} from "../../context/AppContext"; import {AppContextType} from "../../context/@types/appContextTypes"; -import {useSelector} from "react-redux"; -import {oidcSelector} from "../../auth"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; type NewUserModalProps = { @@ -30,6 +30,7 @@ type NewUserModalProps = { export default function NewUserModal(props: NewUserModalProps) { const { alerting } = useContext(AppContext) as AppContextType; + const { isOidc } = useContext(AuthBridgeContext) as AuthContextType; const [username, setUsername] = useState() const [password, setPassword] = useState() const [email, setEmail] = useState() @@ -40,7 +41,6 @@ export default function NewUserModal(props: NewUserModalProps) { const [tester, setTester] = useState(true) const [uploader, setUploader] = useState(false) const [manager, setManager] = useState(false) - const isOidc = useSelector(oidcSelector) !== undefined; const valid = username && (password || isOidc) && email && /^.+@.+\..+$/.test(email) useEffect(() => { setUsername(undefined) diff --git a/horreum-web/src/domain/user/Profile.tsx b/horreum-web/src/domain/user/Profile.tsx index db9e426ca..9144c8c53 100644 --- a/horreum-web/src/domain/user/Profile.tsx +++ b/horreum-web/src/domain/user/Profile.tsx @@ -1,10 +1,10 @@ -import { useSelector } from "react-redux" - -import { keycloakSelector } from "../../auth" - import { Button, Form, FormGroup } from "@patternfly/react-core" import TeamSelect, { Team } from "../../components/TeamSelect" +import {useContext} from "react"; +import {AuthBridgeContext} from "../../context/AuthBridgeContext"; +import {AuthContextType} from "../../context/@types/authContextTypes"; +import {useAuth} from "react-oidc-context"; type ProfileProps = { defaultRole: Team @@ -12,14 +12,17 @@ type ProfileProps = { } export default function Profile(props: ProfileProps) { - const keycloak = useSelector(keycloakSelector) + const { isOidc, isAuthenticated } = useContext(AuthBridgeContext) as AuthContextType; + const auth = useAuth() + return ( - {keycloak && ( + {isOidc && isAuthenticated && (