Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/bookkeeping.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
flps,
home,
about,
header,
eos-report,
data-passes,
simulation-passes,
Expand Down
13 changes: 13 additions & 0 deletions lib/public/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,19 @@ export default class Model extends Observable {

// Navbar dropdown menus
this.dropdownMenu = false;
this.window.document.addEventListener('click', ({ target }) => {
if (this.dropdownMenu && !target.closest?.('#navbar')) {
this.clearDropdownMenu();
}
});
this.window.document.addEventListener('keydown', ({ key }) => {
if (key === 'Escape' && this.dropdownMenu) {
const closedDropdownKey = this.dropdownMenu;
this.clearDropdownMenu();
// Give the focus back to the button which opened the menu, otherwise it is lost when the focused entry is removed
this.window.document.querySelector(`[data-dropdown-trigger="${closedDropdownKey}"]`)?.focus();
}
});
}

/**
Expand Down
48 changes: 48 additions & 0 deletions lib/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -798,8 +798,45 @@ label {
* x-large : 1920 <= x
*/

/* Navbar: below 1280px the page links collapse into a menu, the create button shrinks to a '+' below 600px */
.navbar-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: calc(100vh - 100%);
overflow-y: auto;
}

.navbar-dropdown .dropdown-menu.navbar-dropdown-menu-left {
left: 0;
right: auto;
}

.navbar-dropdown .menu-title {
color: var(--color-gray-darker);
font-weight: 700;
}

.navbar-menu .menu-item.selected {
font-weight: 700;
box-shadow: inset 4px 0 0 var(--color-primary-dark);
}

.navbar-links, .navbar-create-label {
display: none;
}

.navbar-links .btn, .navbar-create-button {
white-space: nowrap;
}

/* md */
@media screen and (min-width: 600px) {
.navbar-create-label {
display: inline;
}

.columns-3, .columns-2 {
grid-template-columns: repeat(2, 1fr);
}
Expand All @@ -816,6 +853,17 @@ label {
}
}

/* navbar: the whole bar needs about 1240px to fit on one line */
@media screen and (min-width: 1280px) {
.navbar-links {
display: inline-flex;
}

.navbar-menu-toggle {
display: none;
}
}

/* xl */
@media screen and (min-width: 1920px) {
.columns-3-xl {
Expand Down
209 changes: 142 additions & 67 deletions lib/public/components/NavBar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
*/

import { h } from '/js/src/index.js';
import { iconPerson, iconPlus } from '/js/src/icons.js';
import { iconMenu, iconPerson, iconPlus } from '/js/src/icons.js';
import { userPreferencesStore } from '../../utilities/userPreferencesStore.js';
import { frontLink } from '../common/navigation/frontLink.js';
import { absoluteFrontLink } from '../common/navigation/absoluteFrontLink.js';
import { externalLinks } from '../common/navigation/externalLinks.js';
import { configurationService } from '../../services/configurationService.js';
import { SHIFT_TYPES } from '../../domain/enums/ShiftTypes.js';
import { selectionDropdown } from '../common/selection/dropdown/selectionDropdown.js';

/**
Expand Down Expand Up @@ -45,15 +46,25 @@ const basePageTab = (page, content, onclick, isSelected = false) => frontLink(
* @param {string} page The internal page to navigate to
* @param {Function<Event, void>} onclick the html element onclick function
* @param {boolean} isSelected if true component is styled as selected
* @param {object} [options] additional options
* @param {object} [options.params] query parameters of the page to navigate to
* @param {object} [options.attributes] additional html attributes of the link
* @return {Component} The generated link tag
*/
const basePageMenuLink = (content, page, onclick, isSelected = false) => frontLink(
const basePageMenuLink = (
content,
page,
null,
onclick,
isSelected = false,
{ params = null, attributes = {} } = {},
) => frontLink(
content,
page,
params,
{
class: ['menu-item', isSelected ? 'selected' : ''].join(' '),
onclick,
...attributes,
},
);

Expand All @@ -62,14 +73,16 @@ const basePageMenuLink = (content, page, onclick, isSelected = false) => frontLi
* @param {string} content The link to navigate to
* @param {string} href The text to display on the link
* @param {Function<Event, void>} onclick the html element onclick function
* @param {object} [attributes] additional html attributes of the link
* @return {Component} The generated link tag
*/
const baseAbsoluteMenuLink = (content, href, onclick) => absoluteFrontLink(
const baseAbsoluteMenuLink = (content, href, onclick, attributes = {}) => absoluteFrontLink(
content,
href,
{
class: 'menu-item',
onclick,
...attributes,
},
);

Expand All @@ -87,6 +100,21 @@ const aliFlpLinkTab = (aliFlpIndexPageUrl) => absoluteFrontLink(
},
);

const NAVIGATION_PAGES = [
['home', 'Home'],
['log-overview', 'Log Entries'],
['env-overview', 'Environments'],
['lhc-fill-overview', 'LHC Fills'],
['run-overview', 'Runs'],
['lhc-period-overview', 'RCT'],
['statistics', 'Statistics'],
];

const OVERVIEW_PAGES = [
['tag-overview', 'Tag Overview'],
['qc-flag-types-overview', 'QC Flag Types'],
];

const btnGroupsDelimiter = h('.mh1', { style: { 'border-left': '1px solid var(--color-gray-dark)' } });

/**
Expand All @@ -96,16 +124,29 @@ const btnGroupsDelimiter = h('.mh1', { style: { 'border-left': '1px solid var(--
* @param {Function<Event, void>} onclick the html element onclick function
* @param {Component[]} entries entries of the menu
* @param {object} options options of the dropdown menu
* @param {string} [options.key] key of the dropdown, as used by the model to know which one is opened
* @param {object} [options.menuAttributes] attributes of dropdown menu
* @param {boolean} [options.isSelected] inform whether dropdown should be styled as selected
* @param {string} [options.dropdownAttributes] attributes of dropdown button
* @param {string} [options.buttonClass] css class of the dropdown button
* @param {object} [options.buttonAttributes] additional html attributes of the dropdown button
* @return {Component} dropdown menu
*/
const dropdownSubMenu = (dropdownButtonContent, isOpened, onclick, entries, { menuAttributes, isSelected, dropdownAttributes } = {}) =>
const dropdownSubMenu = (
dropdownButtonContent,
isOpened,
onclick,
entries,
{ key, menuAttributes, isSelected, dropdownAttributes, buttonClass = 'btn-tab', buttonAttributes = {} } = {},
) =>
h(`.navbar-dropdown ${isOpened ? '.dropdown-open' : ''}`, dropdownAttributes, [
h('button', {
class: `btn btn-tab ${isSelected ? 'selected' : ''}`,
class: `btn ${buttonClass} ${isSelected ? 'selected' : ''}`,
onclick,
'aria-haspopup': 'true',
'aria-expanded': String(isOpened),
'data-dropdown-trigger': key,
...buttonAttributes,
}, dropdownButtonContent),
h('.dropdown-menu', menuAttributes, entries),
]);
Expand All @@ -118,12 +159,70 @@ const dropdownSubMenu = (dropdownButtonContent, isOpened, onclick, entries, { me
function navBar(model) {
const { page: currentPage } = model.router.params;
const pageTab = (page, content) => basePageTab(page, content, () => model.clearDropdownMenu(), currentPage === page);
const pageMenuLink = (content, page) => basePageMenuLink(content, page, () => model.clearDropdownMenu(), currentPage === page);
const absoluteMenuLink = (content, page) => baseAbsoluteMenuLink(content, page, () => model.clearDropdownMenu());
const pageMenuLink = (content, page, options) =>
basePageMenuLink(content, page, () => model.clearDropdownMenu(), options?.isSelected ?? currentPage === page, options);
const absoluteMenuLink = (content, page, attributes) => baseAbsoluteMenuLink(content, page, () => model.clearDropdownMenu(), attributes);

const aliFlpIndexUrl = configurationService.configuration$.getCurrent().match({
Success: (configuration) => configuration.AliFlpIndexUrl,
Other: () => null,
});

return h('.flex-row.justify-between.items-center.p2.shadow-level2.level2.bg-gray-light', [
h('.f4.gray-darker#navbar-title', 'Bookkeeping'),
h('.btn-group', [
const accountMenu = dropdownSubMenu(iconPerson(), model.isDropdownMenuOpened('account'), () => model.toggleDropdownMenu('account'), [
h('p.mh3.text-ellipsis', `Welcome ${model.session.name}`),
h('hr'),
absoluteMenuLink('Jira', `${externalLinks.ALICE_JIRA}/projects/O2B/summary`),
absoluteMenuLink('GitHub', externalLinks.BOOKKEEPING_GITHUB),
absoluteMenuLink('Support Forum', externalLinks.BOOKKEEPING_SUPPORT_FORUM),
h('hr'),
h('p.mh3.text-ellipsis', 'Preferences'),
h('.gray-darker.flex-row.g3.ph3.pv1', [
h('input', {
id: 'preferences-raw-timestamps',
onclick: () => {
userPreferencesStore.rawTimestamps = !userPreferencesStore.rawTimestamps;
},
type: 'checkbox',
checked: userPreferencesStore.rawTimestamps,
}),
h('label', {
for: 'preferences-raw-timestamps',
}, 'Display raw timestamps'),
]),
h('hr'),
model.session.personid === 0 // Anonymous user has id 0
? h('.mh3', [
h('p', 'Roles selection dropdown'),
selectionDropdown(
model.userRoleSelectionModel,
{ selectorPrefix: 'bkp-roles' },
),
h('p.gray-darker', 'This instance of the application does not require authentication.'),
])
: absoluteMenuLink('Logout', 'https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/logout'),
], { key: 'account', dropdownAttributes: { title: 'User Actions' }, menuAttributes: { class: 'navbar-dropdown-menu-left' } });

return h('#navbar.flex-row.justify-between.items-center.p2.shadow-level2.level2.bg-gray-light.relative', [
h('.flex-row.items-center.g2', [
accountMenu,
h('button#navbar-menu-toggle.btn.btn-tab.navbar-menu-toggle', {
title: 'Navigation menu',
'aria-haspopup': 'true',
'aria-expanded': String(model.isDropdownMenuOpened('navigation')),
'data-dropdown-trigger': 'navigation',
onclick: () => model.toggleDropdownMenu('navigation'),
}, iconMenu()),
h('.f4.gray-darker#navbar-title', 'Bookkeeping'),
]),
model.isDropdownMenuOpened('navigation') && h('.navbar-menu.navbar-menu-toggle.flex-column.p2.bg-white.shadow-level3', [
aliFlpIndexUrl && absoluteMenuLink('ALI FLP', aliFlpIndexUrl, { target: '_blank' }),
...NAVIGATION_PAGES.map(([page, content]) => pageMenuLink(content, page)),
h('hr'),
h('p.mh3.gray-darker.f6', 'Overview'),
...OVERVIEW_PAGES.map(([page, content]) => pageMenuLink(content, page)),
pageMenuLink('About', 'about-overview'),
]),
h('.btn-group.navbar-links', [
...configurationService.configuration$.getCurrent().match({

/**
Expand All @@ -138,66 +237,42 @@ function navBar(model) {
],
Other: () => [],
}),
pageTab('home', 'Home'),
pageTab('log-overview', 'Log Entries'),
pageTab('env-overview', 'Environments'),
pageTab('lhc-fill-overview', 'LHC Fills'),
pageTab('run-overview', 'Runs'),
pageTab('lhc-period-overview', 'RCT'),
pageTab('statistics', 'Statistics'),
dropdownSubMenu('Overview', model.isDropdownMenuOpened('overview'), () => model.toggleDropdownMenu('overview'), [
pageMenuLink('Tag Overview', 'tag-overview'),
pageMenuLink('QC Flag Types', 'qc-flag-types-overview'),
], {
menuAttributes: { style: 'width: max-content;' },
isSelected: ['tag-overview', 'qc-flag-types-overview'].includes(currentPage),
}),
...NAVIGATION_PAGES.map(([page, content]) => pageTab(page, content)),
dropdownSubMenu(
'Overview',
model.isDropdownMenuOpened('overview'),
() => model.toggleDropdownMenu('overview'),
OVERVIEW_PAGES.map(([page, content]) => pageMenuLink(content, page)),
{
key: 'overview',
menuAttributes: { style: 'width: max-content;' },
isSelected: OVERVIEW_PAGES.some(([page]) => page === currentPage),
},
),
pageTab('about-overview', 'About'),
]),

h('.flex-row.g2', [
frontLink(h('.flex-row.items-center.g1', [h('small', iconPlus()), 'Log']), 'log-create', {}, {
id: 'create-log-button',
class: 'btn btn-primary',
title: 'Create a new log',
}),
frontLink(h('.flex-row.items-center.g1', [h('small', iconPlus()), 'EoS report']), 'eos-report-create', {}, {
class: 'btn',
title: 'Create a new end of shift report',
}),
dropdownSubMenu(iconPerson(), model.isDropdownMenuOpened('account'), () => model.toggleDropdownMenu('account'), [
h('p.mh3.text-ellipsis', `Welcome ${model.session.name}`),
h('hr'),
absoluteMenuLink('Jira', `${externalLinks.ALICE_JIRA}/projects/O2B/summary`),
absoluteMenuLink('GitHub', externalLinks.BOOKKEEPING_GITHUB),
absoluteMenuLink('Support Forum', externalLinks.BOOKKEEPING_SUPPORT_FORUM),
h('hr'),
h('p.mh3.text-ellipsis', 'Preferences'),
h('.gray-darker.flex-row.g3.ph3.pv1', [
h('input', {
id: 'preferences-raw-timestamps',
onclick: () => {
userPreferencesStore.rawTimestamps = !userPreferencesStore.rawTimestamps;
},
type: 'checkbox',
checked: userPreferencesStore.rawTimestamps,
}),
h('label', {
for: 'preferences-raw-timestamps',
}, 'Display raw timestamps'),
]),
h('hr'),
model.session.personid === 0 // Anonymous user has id 0
? h('.mh3', [
h('p', 'Roles selection dropdown'),
selectionDropdown(
model.userRoleSelectionModel,
{ selectorPrefix: 'bkp-roles' },
),
h('p.gray-darker', 'This instance of the application does not require authentication.'),
])
: absoluteMenuLink('Logout', 'https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/logout'),
], { dropdownAttributes: { title: 'User Actions' } }),
dropdownSubMenu(
h('.flex-row.items-center.g1', [h('small', iconPlus()), h('span.navbar-create-label', 'Add an entry')]),
model.isDropdownMenuOpened('create'),
() => model.toggleDropdownMenu('create'),
[
pageMenuLink('Log entry', 'log-create', { isSelected: false, attributes: { id: 'create-log-button' } }),
pageMenuLink('On-call log', 'log-create', { isSelected: false, params: { templateKey: 'on-call' } }),
pageMenuLink('RC daily meeting log', 'log-create', { isSelected: false, params: { templateKey: 'rc-daily-meeting' } }),
h('hr'),
h('.menu-title', 'EoS report'),
...SHIFT_TYPES.map((shiftType) =>
pageMenuLink(shiftType, 'eos-report-create', { isSelected: false, params: { shiftType } })),
],
{
key: 'create',
buttonClass: 'btn-primary flex-shrink-0 navbar-create-button',
buttonAttributes: { id: 'create-menu-button', title: 'Create a new log or end of shift report' },
menuAttributes: { style: 'width: max-content;' },
},
),
]),
]);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/public/views/EosReport/EosReportModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export class EosReportModel extends Observable {
this._creationModel = new EosReportCreationModel(shiftType, (logId) => this._model.router.go(`/?page=log-detail&id=${logId}`, true));
this._creationModel.bubbleTo(this);
} else {
// The shift type is mandatory, it is chosen from the navbar menu
this._creationModel = null;
this._model.router.go('/?page=home', true);
}
}

Expand Down
Loading
Loading