diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 6932c7ecb9..478cefefbf 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -6,6 +6,12 @@ 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 { + getRegistrationLoaded, + getRegistration, + getAbleToRegister, + getAgreedLatestToU, +} from "../selectors/registrationSelectors"; import { getOrgProperties, getUserBasicInfo, @@ -20,6 +26,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"; @@ -54,7 +65,11 @@ const Header = () => { const healthStatus = useAppSelector(state => getHealthStatus(state)); const errorCounter = useAppSelector(state => getErrorCount(state)); + 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"; @@ -66,6 +81,10 @@ const Header = () => { setMenuHelp(false); }; + const hideNotificationMenu = () => { + setMenuNotify(false); + }; + const showRegistrationModal = () => { registrationModalRef.current?.open(); }; @@ -138,18 +157,24 @@ 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]); + dispatch(fetchRegistration()); + dispatch(fetchLatestToU()); + dispatch(fetchIsUpToDate()); + }, [dispatch]); + + 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 && registrationLoaded && registration == null) { + showRegistrationModal(); + } + }, [user, registration, registrationLoaded]); return ( <>
@@ -214,9 +239,9 @@ const Header = () => { setMenuNotify(!displayMenuNotify)} className="nav-dd-element"> - {errorCounter !== 0 && ( + {(errorCounter !== 0 || !agreedLatestToU || !isAbleToRegister) && ( - {errorCounter} + {errorCounter + (!agreedLatestToU || !isAbleToRegister ? 1 : 0)} )} @@ -225,6 +250,10 @@ const Header = () => { {displayMenuNotify && ( )} @@ -326,9 +355,18 @@ const MenuLang = ({ handleChangeLanguage }: { handleChangeLanguage: (code: strin const MenuNotify = ({ healthStatus, + registering, + updatedToU, + showRegistrationModal, + hideNotificationMenu, }: { healthStatus: HealthStatus[], + registering: boolean, + updatedToU: boolean, + showRegistrationModal: () => void, + hideNotificationMenu: () => void, }) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -338,6 +376,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 */} @@ -361,6 +406,26 @@ const MenuNotify = ({ )} ))} + {!registering && +
  • + showAdoptersRegistrationModal()} + > + Registration + {t("ADOPTER_REGISTRATION.NOTIFICATION.UNREGISTERED")} + +
  • + } + {registering && !updatedToU && +
  • + showAdoptersRegistrationModal()} + > + Registration + {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": { diff --git a/src/selectors/registrationSelectors.ts b/src/selectors/registrationSelectors.ts new file mode 100644 index 0000000000..aa206892a3 --- /dev/null +++ b/src/selectors/registrationSelectors.ts @@ -0,0 +1,12 @@ +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 +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 new file mode 100644 index 0000000000..bc4d0e2961 --- /dev/null +++ b/src/slices/registrationSlice.ts @@ -0,0 +1,99 @@ +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, + termsVersionAgreed: string, +} + +export type RegistrationState = { + loaded: boolean, + registration: Registration | null, + latestToU: string, + ableToRegister: boolean, + agreedToToU: 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, + latestToU: "uninitialized", + ableToRegister: 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 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: {}, + // These are used for thunks + extraReducers: builder => { + builder + .addCase(fetchRegistration.fulfilled, (state, action: PayloadAction< + Registration + >) => { + state.registration = action.payload; + const updatedState = { + registration: state.registration, + latestToU: state.latestToU, + }; + state.agreedToToU = agreedLatestTerms(state, updatedState); + state.loaded = 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; + }); + }, +}); + +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/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, diff --git a/src/styles/components/_header.scss b/src/styles/components/_header.scss index e33a8e1486..d3fa5a2499 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;