From 48ae5a7acf4f5ca05027000fb5a88faa89fa06e9 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Fri, 1 May 2026 22:17:43 -0600 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 298d1a82a1c3969c5612eb904c42af387b5f4762 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Wed, 10 Jun 2026 16:09:15 -0600 Subject: [PATCH 06/18] Modal now appears properly, and creates 'Registration' notifications in suboptimal cases --- src/components/Header.tsx | 59 ++++++++++++++++++++++++++++-- src/styles/components/_header.scss | 3 ++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index f08a6ff205..ad62cf1834 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -6,7 +6,11 @@ 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 } from "../selectors/registrationSelectors"; +import { + getRegistration, + getIsRegistering, + getAgreedLatestToU, +} from "../selectors/registrationSelectors"; import { getOrgProperties, getUserInformation, @@ -22,6 +26,8 @@ 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"; @@ -54,6 +60,8 @@ const Header = () => { const healthStatus = useAppSelector(state => getHealthStatus(state)); const errorCounter = useAppSelector(state => getErrorCount(state)); + const isUpToDate = useAppSelector(state => getIsRegistering(state)); + const agreedLatestToU = useAppSelector(state => getAgreedLatestToU(state)); const user = useAppSelector(state => getUserInformation(state)); const registration = useAppSelector(state => getRegistration(state)); const orgProperties = useAppSelector(state => getOrgProperties(state)); @@ -65,12 +73,18 @@ const Header = () => { useEffect(() => { dispatch(fetchRegistration()); + dispatch(fetchLatestToU()); + dispatch(fetchIsUpToDate()); }, [dispatch]); const hideMenuHelp = () => { setMenuHelp(false); }; + const hideNotificationMenu = () => { + setMenuNotify(false); + }; + const showRegistrationModal = () => { registrationModalRef.current?.open(); }; @@ -219,9 +233,9 @@ const Header = () => { setMenuNotify(!displayMenuNotify)} className="nav-dd-element"> - {errorCounter !== 0 && ( + {(errorCounter !== 0 || !agreedLatestToU || !isUpToDate) && ( - {errorCounter} + {errorCounter + (!agreedLatestToU || !isUpToDate ? 1 : 0)} )} @@ -230,6 +244,10 @@ const Header = () => { {displayMenuNotify && ( )} @@ -324,8 +342,16 @@ const MenuLang = ({ handleChangeLanguage }: { handleChangeLanguage: (code: strin const MenuNotify = ({ healthStatus, + registering, + updatedToU, + showRegistrationModal, + hideNotificationMenu, }: { healthStatus: HealthStatus[], + registering: boolean, + updatedToU: boolean, + showRegistrationModal: () => void, + hideNotificationMenu: () => void, }) => { const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -336,6 +362,13 @@ const MenuNotify = ({ navigate("/systems/services"); }; + // show Adopter Registration Modal and hide drop down + const showAdoptersRegistrationModal = () => { + showRegistrationModal(); + hideNotificationMenu(); + }; + + return (
    {/* For each service in the serviceList (Background Services) one list item */} @@ -359,6 +392,26 @@ const MenuNotify = ({ )} ))} + {!registering && +
  • + showAdoptersRegistrationModal()} + > + Registration + Unregistered + +
  • + } + {registering && !updatedToU && +
  • + showAdoptersRegistrationModal()} + > + Registration + Updated ToU + +
  • + }
); }; diff --git a/src/styles/components/_header.scss b/src/styles/components/_header.scss index e33a8e1486..f668a451eb 100644 --- a/src/styles/components/_header.scss +++ b/src/styles/components/_header.scss @@ -161,6 +161,9 @@ .dropdown-ul{ width: auto; right: 8px; + .wide-text{ + padding-right: 5px; + } } #error-count{ min-width: 10px; From 7bbefffb1cf63af677a808b214510ee7ac8862f0 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Mon, 13 Jul 2026 13:45:34 -0600 Subject: [PATCH 07/18] Combining useEffects --- src/components/Header.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 12ca737062..d1c818ae9e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -75,12 +75,6 @@ const Header = () => { await dispatch(fetchHealthStatus()); }; - useEffect(() => { - dispatch(fetchRegistration()); - dispatch(fetchLatestToU()); - dispatch(fetchIsUpToDate()); - }, [dispatch]); - const hideMenuHelp = () => { setMenuHelp(false); }; @@ -161,6 +155,10 @@ const Header = () => { }, []); useEffect(() => { + dispatch(fetchRegistration()); + dispatch(fetchLatestToU()); + dispatch(fetchIsUpToDate()); + if (!user) { return; } const isAdmin = user.isAdmin || user.isOrgAdmin; @@ -172,7 +170,7 @@ const Header = () => { if (isAdmin && !isLocalhost && dismissedLongEnough && registration == null) { showRegistrationModal(); } - }, [user, registration]); + }, [user, registration, dispatch]); return ( <>
From 833d154a383013816788b64c37fdd459b1b29456 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Wed, 15 Jul 2026 10:32:19 -0600 Subject: [PATCH 08/18] Moving these dispatches out to prevent possible infinite loops --- src/components/Header.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index d1c818ae9e..97e2aa694b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -158,7 +158,9 @@ const Header = () => { dispatch(fetchRegistration()); dispatch(fetchLatestToU()); dispatch(fetchIsUpToDate()); + }, [dispatch]); + useEffect(() => { if (!user) { return; } const isAdmin = user.isAdmin || user.isOrgAdmin; @@ -170,7 +172,7 @@ const Header = () => { if (isAdmin && !isLocalhost && dismissedLongEnough && registration == null) { showRegistrationModal(); } - }, [user, registration, dispatch]); + }, [user, registration]); return ( <>
From 4720639f9cd0509817294a303488ae93f5f62879 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Wed, 15 Jul 2026 10:32:38 -0600 Subject: [PATCH 09/18] Fixing some tabs vs spaces --- src/components/Header.tsx | 40 +++++++++++++++--------------- src/styles/components/_header.scss | 6 ++--- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 97e2aa694b..be6ef66b96 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -403,26 +403,26 @@ const MenuNotify = ({ )} ))} - {!registering && -
  • - showAdoptersRegistrationModal()} - > - Registration - Unregistered - -
  • - } - {registering && !updatedToU && -
  • - showAdoptersRegistrationModal()} - > - Registration - Updated ToU - -
  • - } + {!registering && +
  • + showAdoptersRegistrationModal()} + > + Registration + Unregistered + +
  • + } + {registering && !updatedToU && +
  • + showAdoptersRegistrationModal()} + > + Registration + Updated ToU + +
  • + } ); }; diff --git a/src/styles/components/_header.scss b/src/styles/components/_header.scss index f668a451eb..d3fa5a2499 100644 --- a/src/styles/components/_header.scss +++ b/src/styles/components/_header.scss @@ -161,9 +161,9 @@ .dropdown-ul{ width: auto; right: 8px; - .wide-text{ - padding-right: 5px; - } + .wide-text{ + padding-right: 5px; + } } #error-count{ min-width: 10px; From f22acfe951c1cace405b0053318eb1331660963f Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Wed, 15 Jul 2026 10:33:17 -0600 Subject: [PATCH 10/18] Adding translation strings to notifications --- src/components/Header.tsx | 5 +++-- .../org/opencastproject/adminui/languages/lang-en_US.json | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index be6ef66b96..36005bc988 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -364,6 +364,7 @@ const MenuNotify = ({ showRegistrationModal: () => void, hideNotificationMenu: () => void, }) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -409,7 +410,7 @@ const MenuNotify = ({ onClick={() => showAdoptersRegistrationModal()} > Registration - Unregistered + {t("ADOPTER_REGISTRATION.NOTIFICATION.UNREGISTERED")} } @@ -419,7 +420,7 @@ const MenuNotify = ({ onClick={() => showAdoptersRegistrationModal()} > Registration - Updated ToU + {t("ADOPTER_REGISTRATION.NOTIFICATION.UPDATED_TOU")} } diff --git a/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json b/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json index fc5c32404c..1adaed5865 100644 --- a/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json +++ b/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json @@ -479,6 +479,10 @@ "DELETE_SUBMIT_STATE": { "TEXT": "Are you sure you want to delete your registration data?" } + }, + "NOTIFICATION": { + "UNREGISTERED": "Unregistered", + "UPDATED_TOU": "Updated ToU" } }, "EVENTS": { From f1197ba7f8527e4c0d24b4e01a5b1cc879d2a9a2 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Wed, 15 Jul 2026 10:39:34 -0600 Subject: [PATCH 11/18] Replacing tabs with spaces --- src/slices/registrationSlice.ts | 60 ++++++++++++++++----------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/slices/registrationSlice.ts b/src/slices/registrationSlice.ts index 37adaa6dcf..73076cdcfc 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -50,51 +50,51 @@ export const fetchIsUpToDate = createAppAsyncThunk("registration/isUpToDate", as }); 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 - >) => { + 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 - >) => { + }) + .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 - >) => { + }) + .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) => { + }) + /* .addCase(fetchHealthStatus.rejected, (state, action) => { state.error = true; - }) */; - }, + }) */; + }, }); const agreedLatestTerms = (_state: WritableDraft, updatedState: Temp) => { From 2d69560f54227261d6a7df3186939b7455a2dc0a Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Wed, 15 Jul 2026 10:40:55 -0600 Subject: [PATCH 12/18] Renaming dumb type to something better --- src/slices/registrationSlice.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slices/registrationSlice.ts b/src/slices/registrationSlice.ts index 73076cdcfc..597ca7a714 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -17,7 +17,7 @@ export type RegistrationState = { error: boolean }; -type Temp = { +type StateUpdate = { registration: Registration | null, latestToU: string, }; @@ -97,7 +97,7 @@ const registrationSlice = createSlice({ }, }); -const agreedLatestTerms = (_state: WritableDraft, updatedState: Temp) => { +const agreedLatestTerms = (_state: WritableDraft, updatedState: StateUpdate) => { if (null != updatedState.registration && "uninitialized" != updatedState.latestToU) { return updatedState.registration.termsVersionAgreed === updatedState.latestToU; } From 8bfd33202fc30616a335c3c7960ab5a46efed644 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Wed, 15 Jul 2026 10:41:03 -0600 Subject: [PATCH 13/18] Removing leftovers --- 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 597ca7a714..dbff0e30cd 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -62,9 +62,6 @@ 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 >) => { @@ -90,10 +87,7 @@ const registrationSlice = createSlice({ >) => { // 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; - }) */; + }); }, }); From e025708922738d881c1043fae29c306359a8c8d6 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Wed, 15 Jul 2026 10:56:05 -0600 Subject: [PATCH 14/18] Renaming isRegistering to something a little more sane. The variable represents whether the registration service can talk to the main registration server. --- src/components/Header.tsx | 10 +++++----- src/selectors/registrationSelectors.ts | 2 +- src/slices/registrationSlice.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 36005bc988..9b1eb64d16 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -8,7 +8,7 @@ import { setSpecificServiceFilter } from "../slices/tableFilterSlice"; import { getErrorCount, getHealthStatus } from "../selectors/healthSelectors"; import { getRegistration, - getIsRegistering, + getAbleToRegister, getAgreedLatestToU, } from "../selectors/registrationSelectors"; import { @@ -64,7 +64,7 @@ const Header = () => { const healthStatus = useAppSelector(state => getHealthStatus(state)); const errorCounter = useAppSelector(state => getErrorCount(state)); - const isUpToDate = useAppSelector(state => getIsRegistering(state)); + const isAbleToRegister = useAppSelector(state => getAbleToRegister(state)); const agreedLatestToU = useAppSelector(state => getAgreedLatestToU(state)); const user = useAppSelector(state => getUserInformation(state)); const registration = useAppSelector(state => getRegistration(state)); @@ -237,9 +237,9 @@ const Header = () => { setMenuNotify(!displayMenuNotify)} className="nav-dd-element"> - {(errorCounter !== 0 || !agreedLatestToU || !isUpToDate) && ( + {(errorCounter !== 0 || !agreedLatestToU || !isAbleToRegister) && ( - {errorCounter + (!agreedLatestToU || !isUpToDate ? 1 : 0)} + {errorCounter + (!agreedLatestToU || !isAbleToRegister ? 1 : 0)} )} @@ -248,7 +248,7 @@ const Header = () => { {displayMenuNotify && ( state.registration.registration; // Are we able to talk to register.opencast.org -export const getIsRegistering = (state: RootState) => state.registration.isRegistering; +export const getAbleToRegister = (state: RootState) => state.registration.ableToRegister; // 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 dbff0e30cd..c525ceb97f 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -12,7 +12,7 @@ export type Registration = { export type RegistrationState = { registration: Registration | null, latestToU: string, - isRegistering: boolean, + ableToRegister: boolean, agreedToToU: boolean, error: boolean }; @@ -26,7 +26,7 @@ type StateUpdate = { const initialState: RegistrationState = { registration: null, latestToU: "uninitialized", - isRegistering: false, + ableToRegister: false, agreedToToU: false, error: false, }; @@ -86,7 +86,7 @@ const registrationSlice = createSlice({ boolean >) => { // This is true if the core can talk to https://register.opencast.org/, false otherwise - state.isRegistering = action.payload; + state.ableToRegister = action.payload; }); }, }); From 20b62d132eed8d55d733a8da92d584f194f6d8c7 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Wed, 15 Jul 2026 11:39:26 -0600 Subject: [PATCH 15/18] These are not needed --- src/slices/registrationSlice.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/slices/registrationSlice.ts b/src/slices/registrationSlice.ts index c525ceb97f..fcc8e6ec41 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -53,11 +53,6 @@ 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 => { @@ -98,7 +93,5 @@ const agreedLatestTerms = (_state: WritableDraft, updatedStat return false; }; -export const { setError } = registrationSlice.actions; - // Export the slice reducer as the default export export default registrationSlice.reducer; From 3247ac4313238bbb5c821d66b1766d532ffb732e Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Wed, 15 Jul 2026 16:09:36 -0600 Subject: [PATCH 16/18] Only opening the modal if the registration data has loaded, otherwise the modal will open even if the user has already registered. --- src/components/Header.tsx | 6 ++++-- src/selectors/registrationSelectors.ts | 1 + src/slices/registrationSlice.ts | 6 ++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9b1eb64d16..478cefefbf 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -7,6 +7,7 @@ import opencastLogo from "../img/opencast-white.svg?url"; import { setSpecificServiceFilter } from "../slices/tableFilterSlice"; import { getErrorCount, getHealthStatus } from "../selectors/healthSelectors"; import { + getRegistrationLoaded, getRegistration, getAbleToRegister, getAgreedLatestToU, @@ -67,6 +68,7 @@ const Header = () => { const isAbleToRegister = useAppSelector(state => getAbleToRegister(state)); const agreedLatestToU = useAppSelector(state => getAgreedLatestToU(state)); const user = useAppSelector(state => getUserInformation(state)); + const registrationLoaded = useAppSelector(state => getRegistrationLoaded(state)); const registration = useAppSelector(state => getRegistration(state)); const orgProperties = useAppSelector(state => getOrgProperties(state)); const displayTerms = (orgProperties["org.opencastproject.admin.display_terms"] || "false").toLowerCase() === "true"; @@ -169,10 +171,10 @@ const Header = () => { const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; const dismissedLongEnough = !lastDismissed || Date.now() - parseInt(lastDismissed) > THIRTY_DAYS; - if (isAdmin && !isLocalhost && dismissedLongEnough && registration == null) { + if (isAdmin && !isLocalhost && dismissedLongEnough && registrationLoaded && registration == null) { showRegistrationModal(); } - }, [user, registration]); + }, [user, registration, registrationLoaded]); return ( <>
    diff --git a/src/selectors/registrationSelectors.ts b/src/selectors/registrationSelectors.ts index 04f6d3628f..aa206892a3 100644 --- a/src/selectors/registrationSelectors.ts +++ b/src/selectors/registrationSelectors.ts @@ -3,6 +3,7 @@ import { RootState } from "../store"; /** * This file contains selectors regarding information about the registration status */ +export const getRegistrationLoaded = (state: RootState) => state.registration.loaded; // Are we registered at all export const getRegistration = (state: RootState) => state.registration.registration; // Are we able to talk to register.opencast.org diff --git a/src/slices/registrationSlice.ts b/src/slices/registrationSlice.ts index fcc8e6ec41..bc4d0e2961 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -10,6 +10,7 @@ export type Registration = { } export type RegistrationState = { + loaded: boolean, registration: Registration | null, latestToU: string, ableToRegister: boolean, @@ -24,6 +25,7 @@ type StateUpdate = { // Initial state of health status in redux store const initialState: RegistrationState = { + loaded: false, registration: null, latestToU: "uninitialized", ableToRegister: false, @@ -52,8 +54,7 @@ export const fetchIsUpToDate = createAppAsyncThunk("registration/isUpToDate", as const registrationSlice = createSlice({ name: "registration", initialState, - reducers: { - }, + reducers: {}, // These are used for thunks extraReducers: builder => { builder @@ -66,6 +67,7 @@ const registrationSlice = createSlice({ latestToU: state.latestToU, }; state.agreedToToU = agreedLatestTerms(state, updatedState); + state.loaded = true; }) .addCase(fetchLatestToU.fulfilled, (state, action: PayloadAction< string From ecace10a450fcc236f3727bea2323f4665b712e6 Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Thu, 23 Jul 2026 09:14:36 -0600 Subject: [PATCH 17/18] Combining the last of the old utils script into the slice, moving selectors around to match --- src/selectors/registrationSelectors.ts | 15 ++- src/slices/registrationSlice.ts | 173 ++++++++++++++++++++----- src/utils/adopterRegistrationUtils.ts | 108 --------------- 3 files changed, 151 insertions(+), 145 deletions(-) delete mode 100644 src/utils/adopterRegistrationUtils.ts diff --git a/src/selectors/registrationSelectors.ts b/src/selectors/registrationSelectors.ts index aa206892a3..b5a912f1ad 100644 --- a/src/selectors/registrationSelectors.ts +++ b/src/selectors/registrationSelectors.ts @@ -3,10 +3,17 @@ import { RootState } from "../store"; /** * This file contains selectors regarding information about the registration status */ -export const getRegistrationLoaded = (state: RootState) => state.registration.loaded; +export const getRegistrationLoaded = (state: RootState) => state.registration.registrationLoaded; // Are we registered at all export const getRegistration = (state: RootState) => state.registration.registration; + +export const getStatisticsLoaded = (state: RootState) => state.registration.statisticsLoaded; +// Gather the system statistics reportable to the registration server +export const getStatistics = (state: RootState) => state.registration.statistics; + // Are we able to talk to register.opencast.org -export const getAbleToRegister = (state: RootState) => state.registration.ableToRegister; -// Does our registration match the latest ToU on the core -export const getAgreedLatestToU = (state: RootState) => state.registration.agreedToToU; +export const getAbleToRegister = (state: RootState) => state.registration.registrationLoaded && + state.registration.ableToRegister; +export const getAgreedLatestToU = (state: RootState) => state.registration.registrationLoaded && + state.registration.registration != null && // this is correct because the *endpoint* returns a bare 'null' when the cluster isn't registered + state.registration.registration.termsVersionAgreed == state.registration.latestToU; diff --git a/src/slices/registrationSlice.ts b/src/slices/registrationSlice.ts index bc4d0e2961..c553b815cb 100644 --- a/src/slices/registrationSlice.ts +++ b/src/slices/registrationSlice.ts @@ -1,35 +1,105 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import axios from "axios"; -import { WritableDraft } from "immer"; import { createAppAsyncThunk } from "../createAsyncThunkWithTypes"; export type Registration = { - agreedToPolicy: boolean, - registered: boolean, + contactMe: boolean, + systemType: string, + allowsStatistics: boolean, + allowsErrorReports: boolean, + organisationName: string, + departmentName: string, + country: string, + postalCode: string, + city: string, + firstName: string, + lastName: string, + street: string, + streetNo: string, + email: string, termsVersionAgreed: string, -} + agreedToPolicy: boolean, + dateModified: string, + dateCreated: string, +}; + +// FIXME: This could be expanded to have all the fields, it's missing the nested host stuff +export type Statistics = { + statistics: { + /* the linter doesn't like these, but those are the names. Should we change them? + adopter_key: string, + statistics_key: string, + ca_count: number, + event_count: number, + job_count: number, + series_count: number, + tenant_count: number, + total_minutes: number, + user_count: number, */ + version: string, + } +}; export type RegistrationState = { - loaded: boolean, - registration: Registration | null, + registrationLoaded: boolean, + registration: Registration, + statisticsLoaded: boolean, + statistics: Statistics, latestToU: string, ableToRegister: boolean, - agreedToToU: boolean, + agreedToPolicy: boolean, error: boolean }; -type StateUpdate = { - registration: Registration | null, - latestToU: string, -}; - // Initial state of health status in redux store const initialState: RegistrationState = { - loaded: false, - registration: null, + registrationLoaded: false, + registration: { + contactMe: false, + systemType: "", + allowsStatistics: false, + allowsErrorReports: false, + organisationName: "", + departmentName: "", + country: "", + postalCode: "", + city: "", + firstName: "", + lastName: "", + street: "", + streetNo: "", + email: "", + termsVersionAgreed: "", + agreedToPolicy: false, + dateModified: "", + dateCreated: "", + }, + statisticsLoaded: false, + statistics: { + statistics: { + /* the linter doesn't like these, but those are the names. Should we change them? + adopter_key: "", + statistics_key: "string", + ca_count: -1, + event_count: -1, + job_count: -1, + series_count: -1, + tenant_count: -1, + total_minutes: -1, + user_count: -1,*/ + version: "", + }, + /* There are other keys here, like so + tobira: { + insert arbitrary stats data + }, + But these, and other keys, are not always present and depend *entirely* on local config + Thus, we omit them here + */ + }, latestToU: "uninitialized", ableToRegister: false, - agreedToToU: false, + agreedToPolicy: false, error: false, }; @@ -51,6 +121,54 @@ export const fetchIsUpToDate = createAppAsyncThunk("registration/isUpToDate", as return res.data; }); +// This is whether the statistics used in the registration modal. +export const fetchStatistics = createAppAsyncThunk("registration/statistics", async () => { + const res = await axios.get("/admin-ng/adopter/statistics"); + return res.data; +}); + + + // post request for adopter information + export const postAdopterRegistration = async ( + values: Registration, + ) => { + // build body + const body = new URLSearchParams(); + body.append("contactMe", values.contactMe.toString()); + body.append("systemType", values.systemType); + body.append("allowsStatistics", values.allowsStatistics.toString()); + body.append("allowsErrorReports", values.allowsErrorReports.toString()); + body.append("organisationName", values.organisationName); + body.append("departmentName", values.departmentName); + body.append("country", values.country); + body.append("postalCode", values.postalCode); + body.append("city", values.city); + body.append("firstName", values.firstName); + body.append("lastName", values.lastName); + body.append("street", values.street); + body.append("streetNo", values.streetNo); + body.append("email", values.email); + body.append("agreedToPolicy", values.agreedToPolicy.toString()); + body.append("registered", "true"); + + // save adopter information and return next state + await axios.post("/admin-ng/adopter/registration", body, { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + }; + +// delete adopter information +export const deleteAdopterRegistration = async () => { + // delete adopter information + await axios.delete("/admin-ng/adopter/registration", { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); +}; + const registrationSlice = createSlice({ name: "registration", initialState, @@ -62,38 +180,27 @@ const registrationSlice = createSlice({ Registration >) => { state.registration = action.payload; - const updatedState = { - registration: state.registration, - latestToU: state.latestToU, - }; - state.agreedToToU = agreedLatestTerms(state, updatedState); - state.loaded = true; + state.registrationLoaded = true; }) .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.ableToRegister = action.payload; + }) + .addCase(fetchStatistics.fulfilled, (state, action: PayloadAction< + Statistics + >) => { + state.statistics = action.payload; + state.statisticsLoaded = true; }); }, }); -const agreedLatestTerms = (_state: WritableDraft, updatedState: StateUpdate) => { - if (null != updatedState.registration && "uninitialized" != updatedState.latestToU) { - return updatedState.registration.termsVersionAgreed === updatedState.latestToU; - } - return false; -}; - // Export the slice reducer as the default export export default registrationSlice.reducer; diff --git a/src/utils/adopterRegistrationUtils.ts b/src/utils/adopterRegistrationUtils.ts deleted file mode 100644 index f51aaeada5..0000000000 --- a/src/utils/adopterRegistrationUtils.ts +++ /dev/null @@ -1,108 +0,0 @@ -import axios from "axios"; - -// get information about adopter -export const fetchAdopterRegistration = async () => { - type FetchRegistration = { - contactMe: boolean, - allowsStatistics: boolean, - allowsErrorReports: boolean, - agreedToPolicy: boolean, - registered: boolean, - termsVersionAgreed: string, - deleteMe: boolean, - }; - // fetch current information about adopter - const response = await axios.get("/admin-ng/adopter/registration"); - - return response.data; -}; - -// get statistics information about adopter -export const fetchAdopterStatisticsSummary = async () => { - type FetchSummary = { - general: { - contact_me: boolean, - send_errors: boolean, - send_usage: boolean, - }, - statistics: { - 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, - }[] - }, - }; - const response = await axios.get("/admin-ng/adopter/summary"); - - return response.data; -}; - -export type Registration = { - contactMe: boolean, - systemType: string, - allowsStatistics: boolean, - allowsErrorReports: boolean, - organisationName: string, - departmentName: string, - country: string, - postalCode: string, - city: string, - firstName: string, - lastName: string, - street: string, - streetNo: string, - email: string, - agreedToPolicy: boolean, -} - -// post request for adopter information -export const postRegistration = async ( - values: Registration, -) => { - // build body - const body = new URLSearchParams(); - body.append("contactMe", values.contactMe.toString()); - body.append("systemType", values.systemType); - body.append("allowsStatistics", values.allowsStatistics.toString()); - body.append("allowsErrorReports", values.allowsErrorReports.toString()); - body.append("organisationName", values.organisationName); - body.append("departmentName", values.departmentName); - body.append("country", values.country); - body.append("postalCode", values.postalCode); - body.append("city", values.city); - body.append("firstName", values.firstName); - body.append("lastName", values.lastName); - body.append("street", values.street); - body.append("streetNo", values.streetNo); - body.append("email", values.email); - body.append("agreedToPolicy", values.agreedToPolicy.toString()); - body.append("registered", "true"); - - // save adopter information and return next state - await axios.post("/admin-ng/adopter/registration", body, { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); -}; - -// delete adopter information -export const deleteAdopterRegistration = async () => { - // delete adopter information - await axios.delete("/admin-ng/adopter/registration", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }); -}; From 5a53c12f50c4560d02fa585ca76df80a2815082a Mon Sep 17 00:00:00 2001 From: Greg Logan Date: Thu, 23 Jul 2026 13:45:53 -0600 Subject: [PATCH 18/18] Harmonizing formatting to existing file structures --- src/components/Header.tsx | 6 +- src/components/shared/RegistrationModal.tsx | 70 ++++++++++++--------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 478cefefbf..44e4e3ad6a 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -27,9 +27,9 @@ import { useHotkeys } from "react-hotkeys-hook"; import { useAppDispatch, useAppSelector } from "../store"; import { HealthStatus, fetchHealthStatus } from "../slices/healthSlice"; import { - fetchRegistration, - fetchLatestToU, - fetchIsUpToDate, + fetchRegistration, + fetchLatestToU, + fetchIsUpToDate, } from "../slices/registrationSlice"; import { UserInfoState } from "../slices/userInfoSlice"; import { Tooltip } from "./shared/Tooltip"; diff --git a/src/components/shared/RegistrationModal.tsx b/src/components/shared/RegistrationModal.tsx index 3ccda2b80d..44becd464e 100644 --- a/src/components/shared/RegistrationModal.tsx +++ b/src/components/shared/RegistrationModal.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Formik } from "formik"; +import { useAppDispatch, useAppSelector } from "../../store"; import { Field } from "./Field"; import TermsOfUsePage from "./modals/TermsOfUsePage"; import { countries, states, systemTypes } from "../../configs/adopterRegistrationConfig"; @@ -8,11 +9,18 @@ import cn from "classnames"; import { AdopterRegistrationSchema } from "../../utils/validate"; import { Registration, + Statistics, + fetchRegistration, + fetchStatistics, + postAdopterRegistration, deleteAdopterRegistration, - fetchAdopterRegistration, - fetchAdopterStatisticsSummary, - postRegistration, -} from "../../utils/adopterRegistrationUtils"; +} from "../../slices/registrationSlice"; +import { + getRegistrationLoaded, + getRegistration, + getStatisticsLoaded, + getStatistics, +} from "../../selectors/registrationSelectors"; import ModalContent from "./modals/ModalContent"; import { Modal, ModalHandle } from "./modals/Modal"; import { ParseKeys } from "i18next"; @@ -47,11 +55,17 @@ const RegistrationModal = ({ const RegistrationModalContent = () => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const registrationLoaded = useAppSelector(state => getRegistrationLoaded(state)); + const registration = useAppSelector(state => getRegistration(state)); + const statisticsLoaded = useAppSelector(state => getStatisticsLoaded(state)); + const statistics = useAppSelector(state => getStatistics(state)); // current state of the modal that is shown const [state, setState] = useState("information"); // initial values for Formik - const [initialValues, setInitialValues] = useState({ + const [initialValues, setInitialValues] = useState({ contactMe: false, systemType: "", allowsStatistics: false, @@ -66,20 +80,27 @@ const RegistrationModalContent = () => { street: "", streetNo: "", email: "", + termsVersionAgreed: "", agreedToPolicy: false, + dateModified: "", + dateCreated: "", registered: false, + statistics: null, }); - const [statisticsSummary, setStatisticsSummary] = useState<{ - general: { [key: string]: unknown }, - statistics: { [key: string]: unknown }, - }>(); + useEffect(() => { + if (!registrationLoaded) { + dispatch(fetchRegistration()); + } + if (!statisticsLoaded) { + dispatch(fetchStatistics()); + } + }, [registrationLoaded, statisticsLoaded, dispatch]); useEffect(() => { - fetchRegistrationInfos().then(r => console.log(r)); - fetchStatisticSummary(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + setInitialValues(initialValues => ({ ...initialValues, ...registration, statistics: { ...statistics } })); + }, [registration, statistics]); + const onClickContinue = () => { // if state is deleteSubmit then delete infos about adaptor else show next state @@ -90,23 +111,13 @@ const RegistrationModalContent = () => { } }; - const fetchRegistrationInfos = async () => { - const registrationInfo = await fetchAdopterRegistration(); - - // merge response into initial values for formik - setInitialValues({ ...initialValues, ...registrationInfo }); - }; - - const fetchStatisticSummary = async () => { - const info = await fetchAdopterStatisticsSummary(); - - setStatisticsSummary(info); - }; const handleSubmit = (values: Registration) => { // post request for adopter information - postRegistration(values) + postAdopterRegistration(values) .then(() => { + // Refetch the registration data since otherwise what we just submitted does not show up if the user immediately returns to the modal + dispatch(fetchRegistration()); // show thank you state return setState(states[state].nextState[0] as keyof typeof states); }) @@ -616,7 +627,10 @@ const RegistrationModalContent = () => {

    {t("ADOPTER_REGISTRATION.MODAL.SUMMARY_STATE.GENERAL_HEADER")}

    -									{JSON.stringify(formik.values, null, "\t")}
    +									{JSON.stringify(Object.fromEntries(
    +										Object.entries(formik.values)
    +										.filter(([key, _]) => key != "statistics"))
    +									, null, "\t")}
     								

    @@ -626,7 +640,7 @@ const RegistrationModalContent = () => {

    {t("ADOPTER_REGISTRATION.MODAL.SUMMARY_STATE.STATS_HEADER")}

    -										{JSON.stringify(statisticsSummary?.statistics, null, "\t")}
    +										{JSON.stringify(formik.values.statistics, null, "\t")}