Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move maintenance banner to top of the screen #7421

Merged
merged 10 commits into from
Nov 2, 2023
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
### Fixed
- Searching the segments in the sidebar will highlight newly focused segments properly now. [#7406](https://github.com/scalableminds/webknossos/pull/7406)
- Fixed a bug when opening a task for which a mag restriction exists. The bug only occurred when the referenced mag didn't exist in the dataset. [#7403](https://github.com/scalableminds/webknossos/pull/7403)
- Fixed styling issues with the maintenance banner so that it no longer overlaps other menus, tabs, and buttons. [#7421](https://github.com/scalableminds/webknossos/pull/7421)

### Removed

Expand Down
210 changes: 115 additions & 95 deletions frontend/javascripts/maintenance_banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,123 +4,143 @@ import {
} from "admin/admin_rest_api";
import { Alert } from "antd";
import FormattedDate from "components/formatted_date";
import { useFetch, useInterval } from "libs/react_helpers";
import { sleep } from "libs/utils";
import { useInterval } from "libs/react_helpers";
import _ from "lodash";
import constants from "oxalis/constants";
import { setNavbarHeightAction } from "oxalis/model/actions/ui_actions";
import { setActiveUserAction } from "oxalis/model/actions/user_actions";
import { Store } from "oxalis/singletons";
import { OxalisState } from "oxalis/store";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { MaintenanceInfo } from "types/api_flow_types";

const INITIAL_DELAY = 5000;
const INTERVAL_TO_FETCH_MAINTENANCES_MS = 60000;
const INTERVAL_TO_FETCH_MAINTENANCES_MS = 60000; // 1min

export function MaintenanceBanner() {
const BANNER_STYLE: React.CSSProperties = {
position: "absolute",
top: 0,
left: 0,
height: constants.MAINTENANCE_BANNER_HEIGHT,
};

function setNavbarHeight(newNavbarHeight: number) {
Store.dispatch(setNavbarHeightAction(newNavbarHeight));
document.documentElement.style.setProperty("--navbar-height", `${newNavbarHeight}px`);
}

function UpcomingMaintenanceBanner({ maintenanceInfo }: { maintenanceInfo: MaintenanceInfo }) {
const activeUser = useSelector((state: OxalisState) => state.activeUser);
const { isInAnnotationView } = useSelector((state: OxalisState) => state.uiInformation);
const topPaddingForNavbar = constants.NAVBAR_HEIGHT;
const statusBarHeight = 20;
const [currentAndUpcomingMaintenances, setCurrentAndUpcomingMaintenances] = useState<
Array<MaintenanceInfo>
>([]);
const [position, setPosition] = useState<Object>({ top: topPaddingForNavbar });
const [isTop, setIsTop] = useState(true);

// Do an initial fetch of the maintenance status so that users are notified
// quickly in case of ongoing maintenances.
useFetch(
async () => {
await sleep(INITIAL_DELAY);
setCurrentAndUpcomingMaintenances(await listCurrentAndUpcomingMaintenances());
},
null,
[],
);
// Also poll regularly.
useInterval(async () => {
setCurrentAndUpcomingMaintenances(await listCurrentAndUpcomingMaintenances());
}, INTERVAL_TO_FETCH_MAINTENANCES_MS);
const activeUsersLatestAcknowledgedMaintenance =
activeUser?.novelUserExperienceInfos.latestAcknowledgedMaintenanceInfo;
const { startTime, endTime, message } = maintenanceInfo;

const startDate = new Date(startTime);
const endDate = new Date(endTime);
const endDateFormat = startDate.getDate() === endDate.getDate() ? "HH:mm" : "YYYY-MM-DD HH:mm";

const saveUserClosedMaintenanceInfo = (closestUpcomingMaintenance: MaintenanceInfo) => {
if (activeUser == null) return;

const [nextMaintenanceAcknowledged] = updateNovelUserExperienceInfos(activeUser, {
latestAcknowledgedMaintenanceInfo: closestUpcomingMaintenance.id,
});
Store.dispatch(setActiveUserAction(nextMaintenanceAcknowledged));
};

const toggleTopOrBottomPosition = () => {
setPosition(isTop ? { top: topPaddingForNavbar } : { bottom: statusBarHeight });
setIsTop(!isTop);
};
return (
<Alert
message={
<div>
Upcoming maintenance: <FormattedDate timestamp={startTime} /> until{" "}
<FormattedDate timestamp={endTime} format={endDateFormat} />. {message}
</div>
}
type="info"
banner
style={BANNER_STYLE}
closable
onClose={() => {
saveUserClosedMaintenanceInfo(maintenanceInfo);
setNavbarHeight(constants.DEFAULT_NAVBAR_HEIGHT);
}}
/>
);
}

const getClosestUpcomingMaintenanceBanner = () => {
if (activeUser == null) return null; // upcoming maintenances are only shown after login
const currentTime = Date.now();
const closestUpcomingMaintenance = currentAndUpcomingMaintenances
?.filter((maintenance) => maintenance.startTime > currentTime)
.sort((a, b) => a.startTime - b.startTime)[0];
if (
closestUpcomingMaintenance == null ||
activeUsersLatestAcknowledgedMaintenance === closestUpcomingMaintenance.id
)
return null;
const startDate = new Date(closestUpcomingMaintenance.startTime);
const endDate = new Date(closestUpcomingMaintenance.endTime);
const endDateFormat = startDate.getDate() === endDate.getDate() ? "HH:mm" : "YYYY-MM-DD HH:mm";
return (
<Alert
message={
<div>
Upcoming maintenance: <FormattedDate timestamp={closestUpcomingMaintenance.startTime} />{" "}
until{" "}
<FormattedDate timestamp={closestUpcomingMaintenance.endTime} format={endDateFormat} />.{" "}
{closestUpcomingMaintenance.message}
</div>
}
type="info"
closable
banner
onClose={() => saveUserClosedMaintenanceInfo(closestUpcomingMaintenance)}
/>
);
};
function CurrentMaintenanceBanner({ maintenanceInfo }: { maintenanceInfo: MaintenanceInfo }) {
const { endTime, message } = maintenanceInfo;

const getCurrentMaintenanceBanner = () => {
const currentTime = Date.now();
const currentMaintenance = currentAndUpcomingMaintenances.find(
(maintenance) => maintenance.startTime < currentTime,
);
if (currentMaintenance == null) return;
return (
<Alert
message={
<>
Currently under maintenance, scheduled until{" "}
<FormattedDate timestamp={currentMaintenance.endTime} />. {currentMaintenance.message}
</>
}
type="warning"
banner
onMouseEnter={() => {
if (isInAnnotationView) {
toggleTopOrBottomPosition();
}
}}
style={{ ...position, position: isInAnnotationView ? "absolute" : "sticky" }}
/>
return (
<Alert
message={
<>
Currently under maintenance, scheduled until <FormattedDate timestamp={endTime} />.{" "}
{message}
</>
}
type="warning"
banner
style={BANNER_STYLE}
/>
);
}

export function MaintenanceBanner() {
const activeUser = useSelector((state: OxalisState) => state.activeUser);

const [closestUpcomingMaintenance, setClosestUpcomingMaintenance] = useState<
MaintenanceInfo | undefined
>(undefined);
const [currentMaintenance, setCurrentMaintenance] = useState<MaintenanceInfo | undefined>(
undefined,
);

async function pollMaintenances() {
const newScheduledMaintenances = await listCurrentAndUpcomingMaintenances();

const closestUpcomingMaintenance = newScheduledMaintenances
.filter((maintenance) => maintenance.startTime > Date.now())
.filter(
(maintenance) =>
maintenance.id !== activeUser?.novelUserExperienceInfos.latestAcknowledgedMaintenanceInfo,
)
.sort((a, b) => a.startTime - b.startTime);

const currentMaintenance = newScheduledMaintenances.find(
(maintenance) => maintenance.startTime < Date.now(),
);
};

if (currentAndUpcomingMaintenances.length === 0) return null;
const currentlyUnderMaintenanceBanner = getCurrentMaintenanceBanner();
if (currentlyUnderMaintenanceBanner != null) {
return currentlyUnderMaintenanceBanner;
setCurrentMaintenance(currentMaintenance);
setClosestUpcomingMaintenance(_.first(closestUpcomingMaintenance));
}
const upcomingMaintenanceBanners = getClosestUpcomingMaintenanceBanner();
return upcomingMaintenanceBanners == null ? null : upcomingMaintenanceBanners;

useEffect(() => {
if (currentMaintenance || closestUpcomingMaintenance) {
setNavbarHeight(constants.DEFAULT_NAVBAR_HEIGHT + constants.MAINTENANCE_BANNER_HEIGHT);
}

if (currentMaintenance == null && closestUpcomingMaintenance == null) {
// Reset Navbar height if maintenance is over
setNavbarHeight(constants.DEFAULT_NAVBAR_HEIGHT);
}
}, [currentMaintenance, closestUpcomingMaintenance]);

useEffect(() => {
// Do an initial fetch of the maintenance status so that users are notified
// quickly in case of ongoing maintenances.
setTimeout(pollMaintenances, INITIAL_DELAY);
}, []);

// Also poll regularly.
useInterval(pollMaintenances, INTERVAL_TO_FETCH_MAINTENANCES_MS);

if (currentMaintenance) {
return <CurrentMaintenanceBanner maintenanceInfo={currentMaintenance} />;
}

if (closestUpcomingMaintenance && activeUser !== null) {
return <UpcomingMaintenanceBanner maintenanceInfo={closestUpcomingMaintenance} />;
}

return null;
}
46 changes: 41 additions & 5 deletions frontend/javascripts/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import messages from "messages";
import { PricingEnforcedSpan } from "components/pricing_enforcers";
import { ItemType, MenuItemType, SubMenuType } from "antd/lib/menu/hooks/useItems";
import { MenuClickEventHandler } from "rc-menu/lib/interface";
import constants from "oxalis/constants";
import { MaintenanceBanner } from "maintenance_banner";

const { Header } = Layout;

Expand All @@ -67,6 +69,7 @@ type StateProps = {
othersMayEdit: boolean;
allowUpdate: boolean;
blockedByUser: APIUserCompact | null | undefined;
navbarHeight: number;
};
type Props = OwnProps & StateProps;
// The user should click somewhere else to close that menu like it's done in most OS menus, anyway. 10 seconds.
Expand Down Expand Up @@ -423,7 +426,13 @@ function getDashboardSubMenu(collapse: boolean): SubMenuType {
};
}

function NotificationIcon({ activeUser }: { activeUser: APIUser }) {
function NotificationIcon({
activeUser,
navbarHeight,
}: {
activeUser: APIUser;
navbarHeight: number;
}) {
const maybeUnreadReleaseCount = useOlvyUnreadReleasesCount(activeUser);

const handleShowWhatsNewView = () => {
Expand All @@ -444,6 +453,8 @@ function NotificationIcon({ activeUser }: { activeUser: APIUser }) {
position: "relative",
display: "flex",
marginRight: 12,
paddingTop:
navbarHeight > constants.DEFAULT_NAVBAR_HEIGHT ? constants.MAINTENANCE_BANNER_HEIGHT : 0,
}}
>
<Tooltip title="See what's new in WEBKNOSSOS" placement="bottomLeft">
Expand Down Expand Up @@ -477,7 +488,12 @@ export const switchTo = async (org: APIOrganization) => {
function LoggedInAvatar({
activeUser,
handleLogout,
}: { activeUser: APIUser; handleLogout: (event: React.SyntheticEvent) => void } & SubMenuProps) {
navbarHeight,
}: {
activeUser: APIUser;
handleLogout: (event: React.SyntheticEvent) => void;
navbarHeight: number;
} & SubMenuProps) {
const { firstName, lastName, organization: organizationName, selectedTheme } = activeUser;
const usersOrganizations = useFetch(getUsersOrganizations, [], []);
const activeOrganization = usersOrganizations.find((org) => org.name === organizationName);
Expand Down Expand Up @@ -536,7 +552,9 @@ function LoggedInAvatar({
<Menu
mode="horizontal"
style={{
lineHeight: "48px",
paddingTop:
navbarHeight > constants.DEFAULT_NAVBAR_HEIGHT ? constants.MAINTENANCE_BANNER_HEIGHT : 0,
lineHeight: `${constants.DEFAULT_NAVBAR_HEIGHT}px`,
}}
theme="dark"
subMenuCloseDelay={subMenuCloseDelay}
Expand Down Expand Up @@ -702,6 +720,7 @@ function Navbar({
othersMayEdit,
blockedByUser,
allowUpdate,
navbarHeight,
}: Props) {
const history = useHistory();

Expand Down Expand Up @@ -773,12 +792,19 @@ function Navbar({
/>,
);
}
trailingNavItems.push(<NotificationIcon key="notification-icon" activeUser={loggedInUser} />);
trailingNavItems.push(
<NotificationIcon
key="notification-icon"
activeUser={loggedInUser}
navbarHeight={navbarHeight}
/>,
);
trailingNavItems.push(
<LoggedInAvatar
key="logged-in-avatar"
activeUser={loggedInUser}
handleLogout={handleLogout}
navbarHeight={navbarHeight}
/>,
);
}
Expand Down Expand Up @@ -808,12 +834,17 @@ function Navbar({
"collapsed-nav-header": collapseAllNavItems,
})}
>
<MaintenanceBanner />
<Menu
mode="horizontal"
selectedKeys={selectedKeys}
onOpenChange={(openKeys) => setIsHelpMenuOpen(openKeys.includes(HELP_MENU_KEY))}
style={{
lineHeight: "48px",
paddingTop:
navbarHeight > constants.DEFAULT_NAVBAR_HEIGHT
? constants.MAINTENANCE_BANNER_HEIGHT
: 0,
lineHeight: `${constants.DEFAULT_NAVBAR_HEIGHT}px`,
}}
theme="dark"
subMenuCloseDelay={subMenuCloseDelay}
Expand All @@ -835,6 +866,10 @@ function Navbar({
style={{
flex: 1,
display: "flex",
paddingTop:
navbarHeight > constants.DEFAULT_NAVBAR_HEIGHT
? constants.MAINTENANCE_BANNER_HEIGHT
: 0,
}}
/>

Expand All @@ -858,6 +893,7 @@ const mapStateToProps = (state: OxalisState): StateProps => ({
othersMayEdit: state.tracing.othersMayEdit,
blockedByUser: state.tracing.blockedByUser,
allowUpdate: state.tracing.restrictions.allowUpdate,
navbarHeight: state.uiInformation.navbarHeight,
});

const connector = connect(mapStateToProps);
Expand Down
3 changes: 2 additions & 1 deletion frontend/javascripts/oxalis/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ const Constants = {
BUCKET_WIDTH: 32,
BUCKET_SIZE: 32 ** 3,
VIEWPORT_WIDTH,
NAVBAR_HEIGHT: 48,
DEFAULT_NAVBAR_HEIGHT: 48,
MAINTENANCE_BANNER_HEIGHT: 38,
// For reference, the area of a large brush size (let's say, 300px) corresponds to
// pi * 300 ^ 2 == 282690.
// We multiply this with 5, since the labeling is not done
Expand Down
Loading