From 48ae5a7acf4f5ca05027000fb5a88faa89fa06e9 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Fri, 1 May 2026 22:17:43 -0600 Subject: [PATCH 1/7] In theory this only opens the registration modal in the right circumstances --- src/components/Header.tsx | 46 +++++-- src/selectors/registrationSelectors.ts | 11 ++ src/slices/registrationSlice.ts | 165 +++++++++++++++++++++++++ src/store.ts | 2 + 4 files changed, 212 insertions(+), 12 deletions(-) create mode 100644 src/selectors/registrationSelectors.ts create mode 100644 src/slices/registrationSlice.ts diff --git a/src/components/Header.tsx b/src/components/Header.tsx index abc054a621..970d74d96c 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -6,6 +6,7 @@ import languages from "../i18n/languages"; import opencastLogo from "../img/opencast-white.svg?url"; import { setSpecificServiceFilter } from "../slices/tableFilterSlice"; import { getErrorCount, getHealthStatus } from "../selectors/healthSelectors"; +import { getRegistration, getIsRegistering, getAgreedLatestToU } from "../selectors/registrationSelectors"; import { getOrgProperties, getUserInformation, @@ -19,6 +20,11 @@ import HotKeyCheatSheet from "./shared/HotKeyCheatSheet"; import { useHotkeys } from "react-hotkeys-hook"; import { useAppDispatch, useAppSelector } from "../store"; import { HealthStatus, fetchHealthStatus } from "../slices/healthSlice"; +import { + fetchRegistration, + fetchLatestToU, + fetchIsUpToDate, +} from "../slices/registrationSlice"; import { UserInfoState } from "../slices/userInfoSlice"; import { Tooltip } from "./shared/Tooltip"; import { HiOutlineTranslate } from "react-icons/hi"; @@ -51,6 +57,9 @@ const Header = () => { const healthStatus = useAppSelector(state => getHealthStatus(state)); const errorCounter = useAppSelector(state => getErrorCount(state)); const user = useAppSelector(state => getUserInformation(state)); + const registration = useAppSelector(state => getRegistration(state)); + const _isRegistering = useAppSelector(state => getIsRegistering(state)); + const _agreedLatestToU = useAppSelector(state => getAgreedLatestToU(state)); const orgProperties = useAppSelector(state => getOrgProperties(state)); const displayTerms = (orgProperties["org.opencastproject.admin.display_terms"] || "false").toLowerCase() === "true"; @@ -58,6 +67,19 @@ const Header = () => { await dispatch(fetchHealthStatus()); }; + if (registration == null) { + dispatch(fetchRegistration()); + } + // dispatch(fetchLatestToU()); + // dispatch(fetchIsUpToDate()); + + const _getLatestToU = async () => { + await dispatch(fetchLatestToU()); + }; + const _getIsUpToDate = async () => { + await dispatch(fetchIsUpToDate()); + }; + const hideMenuHelp = () => { setMenuHelp(false); }; @@ -134,18 +156,18 @@ const Header = () => { }, []); useEffect(() => { - if (!user) { return; } - - const isAdmin = user.isAdmin || user.isOrgAdmin; - const isLocalhost = window.location.hostname === "localhost"; - const lastDismissed = localStorage.getItem("adopterModalDismissed"); - const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; - const dismissedLongEnough = !lastDismissed || Date.now() - parseInt(lastDismissed) > THIRTY_DAYS; - - if (isAdmin && !isLocalhost && dismissedLongEnough) { - showRegistrationModal(); - } - }, [user]); + if (!user) { return; } + + const isAdmin = user.isAdmin || user.isOrgAdmin; + const isLocalhost = window.location.hostname === "localhost"; + const lastDismissed = localStorage.getItem("adopterModalDismissed"); + const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; + const dismissedLongEnough = !lastDismissed || Date.now() - parseInt(lastDismissed) > THIRTY_DAYS; + + if (isAdmin && !isLocalhost && dismissedLongEnough && registration == null) { + showRegistrationModal(); + } + }, [user, registration]); return ( <>
diff --git a/src/selectors/registrationSelectors.ts b/src/selectors/registrationSelectors.ts new file mode 100644 index 0000000000..c2c7ca76c9 --- /dev/null +++ b/src/selectors/registrationSelectors.ts @@ -0,0 +1,11 @@ +import { RootState } from "../store"; + +/** + * This file contains selectors regarding information about the registration status + */ +// Are we registered at all +export const getRegistration = (state: RootState) => state.registration.registration; +// Are we able to talk to register.opencast.org +export const getIsRegistering = (state: RootState) => state.registration.isRegistering; +// Does our registration match the latest ToU on the core +export const getAgreedLatestToU = (state: RootState) => state.registration.agreedToToU; diff --git a/src/slices/registrationSlice.ts b/src/slices/registrationSlice.ts new file mode 100644 index 0000000000..848bf94af8 --- /dev/null +++ b/src/slices/registrationSlice.ts @@ -0,0 +1,165 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import axios from "axios"; +import { WritableDraft } from "immer"; +import { createAppAsyncThunk } from "../createAsyncThunkWithTypes"; + +export type Registration = { + adopterKey: string, + statisticsKey: string, + organisationName: string, + departmentName: string, + firstName: string, + lastName: string, + email: string, + country: string, + postalCode: string, + street: string, + streetNo: string, + contactMe: boolean, + systemType: string, + allowsStatistics: boolean, + allowsErrorReports: boolean, + dateCreated: string, + dateUpdated: string, + agreedToPolicy: boolean, + registered: boolean, + termsVersionAgreed: string, + deleteMe: boolean, +} + +export type Summary = { + general: Registration, + statistics: { + statistics_key: string, + adopter_key: string, + job_count: number, + event_count: number, + series_count: number, + user_count: number, + ca_count: number, + total_minutes: number, + tenant_count: number, + hosts: { + cores: number, + max_load: number, + memory: number, + hostname: string, + disk_space: number, + services: string, + }[], + version: string + } +} + +export type RegistrationState = { + registration: Registration | null, + summary: Summary | null, + latestToU: string, + isRegistering: boolean, + agreedToToU: boolean, + error: boolean +}; + +type Temp = { + registration: Registration | null, + latestToU: string, +}; + +// Initial state of health status in redux store +const initialState: RegistrationState = { + registration: null, + summary: null, + latestToU: "uninitialized", + isRegistering: false, + agreedToToU: false, + error: false, +}; + +// This is the registration itself +export const fetchRegistration = createAppAsyncThunk("registration/fetchRegistration", async () => { + const res = await axios.get("/admin-ng/adopter/registration"); + return res.data; +}); + +// This is the summary +export const fetchSummary = createAppAsyncThunk("registration/fetchSummary", async () => { + const res = await axios.get("/admin-ng/adopter/summary"); + return res.data; +}); + +// This is the latest ToU ID. It's a string like APRIL_2020. +export const fetchLatestToU = createAppAsyncThunk("registration/fetchLatestToU", async () => { + const res = await axios.get("/admin-ng/adopter/latestToU"); + return res.data; +}); + +// This is whether the core can talk to register.opencast.org. +export const fetchIsUpToDate = createAppAsyncThunk("registration/isUpToDate", async () => { + const res = await axios.get("/admin-ng/adopter/isUpToDate"); + return res.data; +}); + +const registrationSlice = createSlice({ + name: "registration", + initialState, + reducers: { + setError(state, action: PayloadAction<{ + error: RegistrationState["error"], + }>) { + state.error = action.payload.error; + }, + }, + // These are used for thunks + extraReducers: builder => { + builder + /* .addCase(fetchRegistration.pending, state => { + state.statusHealth = "loading"; + }) */ + .addCase(fetchRegistration.fulfilled, (state, action: PayloadAction< + Registration + >) => { + state.registration = action.payload; + const updatedState = { + registration: state.registration, + latestToU: state.latestToU, + }; + state.agreedToToU = agreedLatestTerms(state, updatedState); + }) + .addCase(fetchLatestToU.fulfilled, (state, action: PayloadAction< + string + >) => { + state.latestToU = action.payload; + const updatedState = { + registration: state.registration, + latestToU: state.latestToU, + }; + state.agreedToToU = agreedLatestTerms(state, updatedState); + }) + .addCase(fetchSummary.fulfilled, (state, action: PayloadAction< + Summary + >) => { + state.summary = action.payload; + }) + .addCase(fetchIsUpToDate.fulfilled, (state, action: PayloadAction< + boolean + >) => { + // This is true if the core can talk to https://register.opencast.org/, false otherwise + state.isRegistering = action.payload; + }) + /* .addCase(fetchHealthStatus.rejected, (state, action) => { + state.error = true; + }) */; + }, +}); + +const agreedLatestTerms = (_state: WritableDraft, updatedState: Temp) => { + if (null != updatedState.registration && "uninitialized" != updatedState.latestToU) { + return updatedState.registration.termsVersionAgreed === updatedState.latestToU; + } + return false; +}; + +export const { setError } = registrationSlice.actions; + +// Export the slice reducer as the default export +export default registrationSlice.reducer; diff --git a/src/store.ts b/src/store.ts index 76a3f65d9f..0558b56bb1 100644 --- a/src/store.ts +++ b/src/store.ts @@ -15,6 +15,7 @@ import groups from "./slices/groupSlice"; import acls from "./slices/aclSlice"; import themes from "./slices/themeSlice"; import health from "./slices/healthSlice"; +import registration from "./slices/registrationSlice"; import notifications from "./slices/notificationSlice"; import workflows from "./slices/workflowSlice"; import eventDetails from "./slices/eventDetailsSlice"; @@ -64,6 +65,7 @@ const reducers = combineReducers({ acls: persistReducer(aclsPersistConfig, acls), themes: persistReducer(themesPersistConfig, themes), health, + registration, notifications, workflows, eventDetails, From c12f60817d7ab50649f283b380de96d0498dd5de Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Thu, 7 May 2026 14:36:03 -0600 Subject: [PATCH 2/7] Revert me for further work --- src/components/Header.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 970d74d96c..eadfc8cd36 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -6,7 +6,7 @@ import languages from "../i18n/languages"; import opencastLogo from "../img/opencast-white.svg?url"; import { setSpecificServiceFilter } from "../slices/tableFilterSlice"; import { getErrorCount, getHealthStatus } from "../selectors/healthSelectors"; -import { getRegistration, getIsRegistering, getAgreedLatestToU } from "../selectors/registrationSelectors"; +import { getRegistration } from "../selectors/registrationSelectors"; import { getOrgProperties, getUserInformation, @@ -22,8 +22,6 @@ import { useAppDispatch, useAppSelector } from "../store"; import { HealthStatus, fetchHealthStatus } from "../slices/healthSlice"; import { fetchRegistration, - fetchLatestToU, - fetchIsUpToDate, } from "../slices/registrationSlice"; import { UserInfoState } from "../slices/userInfoSlice"; import { Tooltip } from "./shared/Tooltip"; @@ -58,8 +56,6 @@ const Header = () => { const errorCounter = useAppSelector(state => getErrorCount(state)); const user = useAppSelector(state => getUserInformation(state)); const registration = useAppSelector(state => getRegistration(state)); - const _isRegistering = useAppSelector(state => getIsRegistering(state)); - const _agreedLatestToU = useAppSelector(state => getAgreedLatestToU(state)); const orgProperties = useAppSelector(state => getOrgProperties(state)); const displayTerms = (orgProperties["org.opencastproject.admin.display_terms"] || "false").toLowerCase() === "true"; @@ -70,15 +66,6 @@ const Header = () => { if (registration == null) { dispatch(fetchRegistration()); } - // dispatch(fetchLatestToU()); - // dispatch(fetchIsUpToDate()); - - const _getLatestToU = async () => { - await dispatch(fetchLatestToU()); - }; - const _getIsUpToDate = async () => { - await dispatch(fetchIsUpToDate()); - }; const hideMenuHelp = () => { setMenuHelp(false); From 45f695b333ecd3bd352846e6501a1c659f4d3d0a Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Thu, 7 May 2026 20:14:37 -0600 Subject: [PATCH 3/7] Removing summary, since it's not needed here --- src/slices/registrationSlice.ts | 37 --------------------------------- 1 file changed, 37 deletions(-) diff --git a/src/slices/registrationSlice.ts b/src/slices/registrationSlice.ts index 848bf94af8..1ca89d397c 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -27,33 +27,8 @@ export type Registration = { deleteMe: boolean, } -export type Summary = { - general: Registration, - statistics: { - statistics_key: string, - adopter_key: string, - job_count: number, - event_count: number, - series_count: number, - user_count: number, - ca_count: number, - total_minutes: number, - tenant_count: number, - hosts: { - cores: number, - max_load: number, - memory: number, - hostname: string, - disk_space: number, - services: string, - }[], - version: string - } -} - export type RegistrationState = { registration: Registration | null, - summary: Summary | null, latestToU: string, isRegistering: boolean, agreedToToU: boolean, @@ -68,7 +43,6 @@ type Temp = { // Initial state of health status in redux store const initialState: RegistrationState = { registration: null, - summary: null, latestToU: "uninitialized", isRegistering: false, agreedToToU: false, @@ -81,12 +55,6 @@ export const fetchRegistration = createAppAsyncThunk("registration/fetchRegistra return res.data; }); -// This is the summary -export const fetchSummary = createAppAsyncThunk("registration/fetchSummary", async () => { - const res = await axios.get("/admin-ng/adopter/summary"); - return res.data; -}); - // This is the latest ToU ID. It's a string like APRIL_2020. export const fetchLatestToU = createAppAsyncThunk("registration/fetchLatestToU", async () => { const res = await axios.get("/admin-ng/adopter/latestToU"); @@ -134,11 +102,6 @@ const registrationSlice = createSlice({ latestToU: state.latestToU, }; state.agreedToToU = agreedLatestTerms(state, updatedState); - }) - .addCase(fetchSummary.fulfilled, (state, action: PayloadAction< - Summary - >) => { - state.summary = action.payload; }) .addCase(fetchIsUpToDate.fulfilled, (state, action: PayloadAction< boolean From 92f70b3d64c3233305c2c61ff3d63884165d5169 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Thu, 7 May 2026 20:27:34 -0600 Subject: [PATCH 4/7] Switching to useEffect, suggested in the review --- src/components/Header.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index eadfc8cd36..f08a6ff205 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -63,9 +63,9 @@ const Header = () => { await dispatch(fetchHealthStatus()); }; - if (registration == null) { + useEffect(() => { dispatch(fetchRegistration()); - } + }, [dispatch]); const hideMenuHelp = () => { setMenuHelp(false); From 094d207965e9cad43c2c160a709a6b94403e9d11 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Thu, 7 May 2026 20:27:48 -0600 Subject: [PATCH 5/7] Removing these since this part of the service does not care about this. --- src/slices/registrationSlice.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/slices/registrationSlice.ts b/src/slices/registrationSlice.ts index 1ca89d397c..37adaa6dcf 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -4,27 +4,9 @@ import { WritableDraft } from "immer"; import { createAppAsyncThunk } from "../createAsyncThunkWithTypes"; export type Registration = { - adopterKey: string, - statisticsKey: string, - organisationName: string, - departmentName: string, - firstName: string, - lastName: string, - email: string, - country: string, - postalCode: string, - street: string, - streetNo: string, - contactMe: boolean, - systemType: string, - allowsStatistics: boolean, - allowsErrorReports: boolean, - dateCreated: string, - dateUpdated: string, agreedToPolicy: boolean, registered: boolean, termsVersionAgreed: string, - deleteMe: boolean, } export type RegistrationState = { From 6895fade121b65f6e6f5543a9de455be069ffc5a Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Mon, 11 May 2026 14:52:09 -0600 Subject: [PATCH 6/7] As simplified as possible? --- src/selectors/registrationSelectors.ts | 4 -- src/slices/registrationSlice.ts | 58 ++------------------------ 2 files changed, 3 insertions(+), 59 deletions(-) diff --git a/src/selectors/registrationSelectors.ts b/src/selectors/registrationSelectors.ts index c2c7ca76c9..f6d9354405 100644 --- a/src/selectors/registrationSelectors.ts +++ b/src/selectors/registrationSelectors.ts @@ -5,7 +5,3 @@ import { RootState } from "../store"; */ // Are we registered at all export const getRegistration = (state: RootState) => state.registration.registration; -// Are we able to talk to register.opencast.org -export const getIsRegistering = (state: RootState) => state.registration.isRegistering; -// Does our registration match the latest ToU on the core -export const getAgreedLatestToU = (state: RootState) => state.registration.agreedToToU; diff --git a/src/slices/registrationSlice.ts b/src/slices/registrationSlice.ts index 37adaa6dcf..a6ed6b78c5 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -1,6 +1,5 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import axios from "axios"; -import { WritableDraft } from "immer"; import { createAppAsyncThunk } from "../createAsyncThunkWithTypes"; export type Registration = { @@ -10,24 +9,13 @@ export type Registration = { } export type RegistrationState = { - registration: Registration | null, - latestToU: string, - isRegistering: boolean, - agreedToToU: boolean, + registration: boolean | null, error: boolean }; -type Temp = { - registration: Registration | null, - latestToU: string, -}; - // Initial state of health status in redux store const initialState: RegistrationState = { registration: null, - latestToU: "uninitialized", - isRegistering: false, - agreedToToU: false, error: false, }; @@ -37,18 +25,6 @@ export const fetchRegistration = createAppAsyncThunk("registration/fetchRegistra return res.data; }); -// This is the latest ToU ID. It's a string like APRIL_2020. -export const fetchLatestToU = createAppAsyncThunk("registration/fetchLatestToU", async () => { - const res = await axios.get("/admin-ng/adopter/latestToU"); - return res.data; -}); - -// This is whether the core can talk to register.opencast.org. -export const fetchIsUpToDate = createAppAsyncThunk("registration/isUpToDate", async () => { - const res = await axios.get("/admin-ng/adopter/isUpToDate"); - return res.data; -}); - const registrationSlice = createSlice({ name: "registration", initialState, @@ -65,31 +41,10 @@ const registrationSlice = createSlice({ /* .addCase(fetchRegistration.pending, state => { state.statusHealth = "loading"; }) */ - .addCase(fetchRegistration.fulfilled, (state, action: PayloadAction< + .addCase(fetchRegistration.fulfilled, (state, _action: PayloadAction< Registration >) => { - state.registration = action.payload; - const updatedState = { - registration: state.registration, - latestToU: state.latestToU, - }; - state.agreedToToU = agreedLatestTerms(state, updatedState); - }) - .addCase(fetchLatestToU.fulfilled, (state, action: PayloadAction< - string - >) => { - state.latestToU = action.payload; - const updatedState = { - registration: state.registration, - latestToU: state.latestToU, - }; - state.agreedToToU = agreedLatestTerms(state, updatedState); - }) - .addCase(fetchIsUpToDate.fulfilled, (state, action: PayloadAction< - boolean - >) => { - // This is true if the core can talk to https://register.opencast.org/, false otherwise - state.isRegistering = action.payload; + state.registration = true; }) /* .addCase(fetchHealthStatus.rejected, (state, action) => { state.error = true; @@ -97,13 +52,6 @@ const registrationSlice = createSlice({ }, }); -const agreedLatestTerms = (_state: WritableDraft, updatedState: Temp) => { - if (null != updatedState.registration && "uninitialized" != updatedState.latestToU) { - return updatedState.registration.termsVersionAgreed === updatedState.latestToU; - } - return false; -}; - export const { setError } = registrationSlice.actions; // Export the slice reducer as the default export From 75e3a117cd64531b4684e8170aa6433766c47bb2 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Mon, 11 May 2026 15:09:07 -0600 Subject: [PATCH 7/7] Removing commented code --- src/slices/registrationSlice.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/slices/registrationSlice.ts b/src/slices/registrationSlice.ts index a6ed6b78c5..faee20d637 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -38,17 +38,11 @@ const registrationSlice = createSlice({ // These are used for thunks extraReducers: builder => { builder - /* .addCase(fetchRegistration.pending, state => { - state.statusHealth = "loading"; - }) */ .addCase(fetchRegistration.fulfilled, (state, _action: PayloadAction< Registration >) => { state.registration = true; - }) - /* .addCase(fetchHealthStatus.rejected, (state, action) => { - state.error = true; - }) */; + }); }, });