Skip to content
88 changes: 74 additions & 14 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +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,
getIsRegistering,
getAgreedLatestToU,
} from "../selectors/registrationSelectors";
import {
getOrgProperties,
getUserBasicInfo,
Expand All @@ -20,6 +25,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";
Expand Down Expand Up @@ -54,7 +64,10 @@ 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));
const displayTerms = (orgProperties["org.opencastproject.admin.display_terms"] || "false").toLowerCase() === "true";

Expand All @@ -66,6 +79,10 @@ const Header = () => {
setMenuHelp(false);
};

const hideNotificationMenu = () => {
setMenuNotify(false);
};

const showRegistrationModal = () => {
registrationModalRef.current?.open();
};
Expand Down Expand Up @@ -138,18 +155,22 @@ 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());

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, dispatch]);
Comment on lines 157 to +173

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can cause an infinite loop. dispatch(fetchRegistration()); will eventually update registration, which triggers a rerun of the useEffect hook, which causes dispatch(fetchRegistration()); to be run again...

I recommend putting the dispatch(fetch....'s into their own useEffect hook that runs only once on mount. If you need to run them again, you should probably do so conditionally?

return (
<>
<header className="primary-header">
Expand Down Expand Up @@ -214,9 +235,9 @@ const Header = () => {
<Tooltip active={!displayMenuNotify} title={t("SYSTEM_NOTIFICATIONS")}>
<BaseButton onClick={() => setMenuNotify(!displayMenuNotify)} className="nav-dd-element">
<LuBell className="header-icon"/>
{errorCounter !== 0 && (
{(errorCounter !== 0 || !agreedLatestToU || !isUpToDate) && (
<span id="error-count" className="badge">
{errorCounter}
{errorCounter + (!agreedLatestToU || !isUpToDate ? 1 : 0)}
</span>
)}
</BaseButton>
Expand All @@ -225,6 +246,10 @@ const Header = () => {
{displayMenuNotify && (
<MenuNotify
healthStatus={healthStatus}
registering={isUpToDate}
updatedToU={agreedLatestToU}
showRegistrationModal={showRegistrationModal}
hideNotificationMenu={hideNotificationMenu}
/>
)}
</div>
Expand Down Expand Up @@ -326,8 +351,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();
Expand All @@ -338,6 +371,13 @@ const MenuNotify = ({
navigate("/systems/services");
};

// show Adopter Registration Modal and hide drop down
const showAdoptersRegistrationModal = () => {
showRegistrationModal();
hideNotificationMenu();
};


return (
<ul className="dropdown-ul">
{/* For each service in the serviceList (Background Services) one list item */}
Expand All @@ -361,6 +401,26 @@ const MenuNotify = ({
)}
</li>
))}
{!registering &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not using tab indentation here

<li>
<ButtonLikeAnchor
onClick={() => showAdoptersRegistrationModal()}
>
<span className="wide-text">Registration</span>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untranslated strings.

<span className="multi-value multi-value-yellow">Unregistered</span>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multi-value-yellow does not seem to exist (maybe I removed it in my cleaning frenzy?).

</ButtonLikeAnchor>
</li>
}
{registering && !updatedToU &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I am unregistered and the ToU have been updated, there would be two notifications. Is that intended?

<li>
<ButtonLikeAnchor
onClick={() => showAdoptersRegistrationModal()}
>
<span className="wide-text">Registration</span>
<span className="multi-value multi-value-yellow">Updated ToU</span>
</ButtonLikeAnchor>
</li>
}
</ul>
);
};
Expand Down
11 changes: 11 additions & 0 deletions src/selectors/registrationSelectors.ts
Original file line number Diff line number Diff line change
@@ -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;
110 changes: 110 additions & 0 deletions src/slices/registrationSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent use of spaces and tabs in this file.

import axios from "axios";
import { WritableDraft } from "immer";
import { createAppAsyncThunk } from "../createAsyncThunkWithTypes";

export type Registration = {
Comment thread
Arnei marked this conversation as resolved.
agreedToPolicy: boolean,
registered: boolean,
termsVersionAgreed: string,
}

export type RegistrationState = {
registration: Registration | null,
latestToU: string,
isRegistering: boolean,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like isRegistering is a bit confusingly named? This is not about being in the process of registering, is it? Maybe canRegister would be clearer?

agreedToToU: boolean,
error: boolean
};

type Temp = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temp is not a great type name ^^

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,
};

// This is the registration itself
export const fetchRegistration = createAppAsyncThunk("registration/fetchRegistration", async () => {
const res = await axios.get<Registration>("/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<string>("/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<boolean>("/admin-ng/adopter/isUpToDate");
return res.data;
});

const registrationSlice = createSlice({
name: "registration",
initialState,
reducers: {
setError(state, action: PayloadAction<{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see setError nor error being used anywhere. Leftover code?

error: RegistrationState["error"],
}>) {
state.error = action.payload.error;
},
},
// These are used for thunks
extraReducers: builder => {
builder
/* .addCase(fetchRegistration.pending, state => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover code?

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(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<RegistrationState>, 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;
2 changes: 2 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -64,6 +65,7 @@ const reducers = combineReducers({
acls: persistReducer(aclsPersistConfig, acls),
themes: persistReducer(themesPersistConfig, themes),
health,
registration,
notifications,
workflows,
eventDetails,
Expand Down
3 changes: 3 additions & 0 deletions src/styles/components/_header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@
.dropdown-ul{
width: auto;
right: 8px;
.wide-text{
padding-right: 5px;
}
Comment on lines +164 to +166

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.wide-text{
padding-right: 5px;
}
.wide-text{
padding-right: 5px;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're using 2 spaces, but the file uses 4.

}
#error-count{
min-width: 10px;
Expand Down
Loading