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
11 changes: 11 additions & 0 deletions Configuration/webapp/app/api/axiosInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,26 @@
*/

import axios from 'axios';
import { getSessionData } from '~/services/session';

export const API_URL = 'http://localhost:8080/control/api';

const axiosInstance = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
'User-Agent': 'axios 0.21.1',
},
withCredentials: false,
});

axiosInstance.interceptors.request.use(async (config) => {
const { token } = await getSessionData();
if (token) {
config.url = `${config.url}?token=${token}`;
}

return config;
});

export default axiosInstance;
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ interface ConfigNavigatorItemProps {
* @returns {React.ReactElement} ConfigNavigatorItem
*/
const ConfigNavigatorItem: FC<ConfigNavigatorItemProps> = ({ title, onClick, isSelected }) => (
<ListItem style={{ paddingTop: 5, paddingBottom: 5 }} className="config_navigator__item">
<ListItem
style={{ paddingTop: 5, paddingBottom: 5 }}
className={`config_navigator__item ${isSelected ? 'config_navigator__item--selected' : ''} config_key__${title}`}
>
<Link to={`configuration/${BASE_CONFIGURATION_PATH}/${title}`} style={{ width: '100%' }}>
<ListItemButton
onClick={onClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ export const ContentHeader: FC<ContentHeaderProps> = ({ currentPath }) => (
<Typography variant="h5" className="config-page__header__text">
{currentPath}
</Typography>
<UserSection userName="John D." />
<UserSection />
</Toolbar>
);
25 changes: 12 additions & 13 deletions Configuration/webapp/app/components/user-section/UserSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,19 @@
* or submit itself to any jurisdiction.
*/

import { Box, IconButton, Menu, MenuItem, Avatar } from '@mui/material';
import { useState, type FC, type MouseEvent } from 'react';
import { Box, IconButton, Menu, MenuItem, Avatar, Typography } from '@mui/material';
import { useState, type MouseEvent } from 'react';
import { useAuth } from '~/hooks/useAuth';
import { getSessionData } from '~/services/session';

interface UserSectionProps {
userName: string;
}

/**
* UserSection component
* Represents a user section with an avatar and a dropdown menu for user actions.
* @param {UserSectionProps} props - Component props.
* @returns {React.ReactElement} UserSection
*/
export const UserSection: FC<UserSectionProps> = ({ userName }) => {
export const UserSection = () => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

const { name: userName } = useAuth();
const handleClick = (event: MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
Expand All @@ -49,7 +45,7 @@ export const UserSection: FC<UserSectionProps> = ({ userName }) => {
return (
<Box sx={{ flexGrow: 0 }} className="user-section">
<IconButton sx={{ p: 0 }} onClick={handleClick}>
<Avatar>{userName[0]}</Avatar>
<Avatar>{userName?.[0] ?? ''}</Avatar>
</IconButton>
<Menu
anchorEl={anchorEl}
Expand All @@ -65,9 +61,12 @@ export const UserSection: FC<UserSectionProps> = ({ userName }) => {
onClose={handleClose}
className="user-section__menu"
>
<MenuItem onClick={displayProfileData}>Profile</MenuItem>
<MenuItem onClick={handleClose}>My account</MenuItem>
<MenuItem onClick={handleClose}>Logout</MenuItem>
<Box sx={{ p: 1 }}>
<Typography variant="h5">Welcome, {userName}!</Typography>
<MenuItem onClick={displayProfileData}>Profile</MenuItem>
<MenuItem onClick={handleClose}>My account</MenuItem>
<MenuItem onClick={handleClose}>Logout</MenuItem>
</Box>
</Menu>
</Box>
);
Expand Down
38 changes: 38 additions & 0 deletions Configuration/webapp/app/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

import { useEffect, useState } from 'react';
import { getSessionData } from '~/services/session';

export interface Session {
personid: string;
username: string;
name: string;
access: string;
token: string;
}

export const useAuth = (): Session => {
const [session, setSession] = useState<Record<string, string>>({});

useEffect(() => {
const fetchSession = async () => {
const session = await getSessionData();
setSession(session);
};
void fetchSession();
}, []);

return session as unknown as Session;
};
2 changes: 1 addition & 1 deletion Configuration/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc",
"docker:typecheck": "docker compose exec webapp npm run typecheck",
"mocha": "tsx node_modules/.bin/mocha --timeout 20000 --require test/mocha-index.ts test/**/*.spec.ts",
"mocha": "tsx node_modules/.bin/mocha --timeout 40000 --require test/mocha-index.ts test/**/*.spec.ts",
"eslint": "eslint app/**",
"eslint-fix": "eslint --fix app/**",
"prettier": "prettier --write app/**",
Expand Down
41 changes: 41 additions & 0 deletions Configuration/webapp/test/api/axiosInstance.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

import assert from 'assert';

const BAD_REQUEST_ERROR_CODE = 'ERR_BAD_REQUEST';

//test Configuration/webapp/app/api/axiosInstance.ts
import axiosInstance, { API_URL } from '../../app/api/axiosInstance';

describe('axios instance', function () {
this.timeout(20000);

it('should make a GET request and receive a response', async function () {
const response = await axiosInstance.get(`${API_URL}/configurations`);
assert.strictEqual(response.status, 200);
});

it('should handle error response correctly', async function () {
try {
await axiosInstance.get(`${API_URL}/not-existing`);
} catch (error: unknown) {
if (typeof error === 'object' && error !== null && 'code' in error) {
assert.strictEqual(error.code, BAD_REQUEST_ERROR_CODE);
} else {
assert.fail('Error object does not have code property');
}
}
});
});
41 changes: 26 additions & 15 deletions Configuration/webapp/test/public/page-configuration-mocha.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
import assert from 'assert';
import { Page } from 'puppeteer';
import global from '../mocha-index';
import { API_URL } from '~/api/axiosInstance';

describe('`pageRoot` test-suite', function () {
describe('`pageConfiguration` test-suite', function () {
let url: string | null = null;
let page: Page | null = null;

Expand All @@ -35,37 +34,49 @@ describe('`pageRoot` test-suite', function () {
assert.equal('Page is null', 'test suite failed');
return;
}
const res = await fetch(`${API_URL}/configurations`);
const data = await res.json();
await page.goto(url, { waitUntil: 'networkidle0' });

const firstConfigurationRelativePath = data?.[0];
const configNavigatorItems = await page.$$('.config_navigator__item--selected');
assert.strictEqual(configNavigatorItems.length, 1);

const configUrl = `${url}/configuration/${firstConfigurationRelativePath}`;
const classList = await configNavigatorItems[0].evaluate((el) => el.className.split(' '));
const selectedKey = Array.from(classList)
.find((className: string) => className.startsWith('config_key__'))
?.split('__')[1];

await page.goto(configUrl, { waitUntil: 'networkidle0' });
if (!selectedKey) {
assert.equal('No selected key found', 'test suite failed');
return;
}

const location = await page.evaluate(() => window.location);
assert.strictEqual(location.search, '');
assert.strictEqual(location.pathname.includes(selectedKey), true);
});

it('should display proper configuration page header', async function () {
if (page === null || url === null) {
assert.equal('Page is null', 'test suite failed');
return;
}
const res = await fetch(`${API_URL}/configurations`);
const data = await res.json();
await page.goto(url, { waitUntil: 'networkidle0' });

const firstConfigurationRelativePath = data?.[0];
const configNavigatorItems = await page.$$('.config_navigator__item--selected');
assert.strictEqual(configNavigatorItems.length, 1);

const configUrl = `${url}/configuration/${firstConfigurationRelativePath}`;
const classList = await configNavigatorItems[0].evaluate((el) => el.className.split(' '));
const selectedKey = Array.from(classList)
.find((className: string) => className.startsWith('config_key__'))
?.split('__')[1];

await page.goto(configUrl, { waitUntil: 'networkidle0' });
if (!selectedKey) {
assert.equal('No selected key found', 'test suite failed');
return;
}

const configPageHeader = await page.$$('.config-page__header__text');
assert.strictEqual(configPageHeader.length, 1);

const headerText = await page.evaluate((el) => el.textContent, configPageHeader[0]);
assert.strictEqual(headerText, firstConfigurationRelativePath);
const headerText = (await page.evaluate((el) => el.textContent, configPageHeader[0])) ?? '';
assert.strictEqual(headerText.includes(selectedKey), true);
});
});
20 changes: 2 additions & 18 deletions Configuration/webapp/test/public/page-root-mocha.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import assert from 'assert';
import { Page } from 'puppeteer';
import global from '../mocha-index';

describe('`pageConfiguration` test-suite', function () {
describe('`pageRoot` test-suite', function () {
let url: string | null = null;
let page: Page | null = null;

Expand Down Expand Up @@ -128,23 +128,7 @@ describe('`pageConfiguration` test-suite', function () {
return;
}

const res = await fetch('http://localhost:8080/control/api/configurations');
const data = await res.json();

const configNavigatorItems = await page.$$('.config_navigator__item');
assert.strictEqual(configNavigatorItems.length, data?.length ?? 0);
});

it('should display configurations list', async function () {
if (page === null || url === null) {
assert.equal('Page is null', 'test suite failed');
return;
}

const res = await fetch('http://localhost:8080/control/api/configurations');
const data = await res.json();

const configNavigatorItems = await page.$$('.config_navigator__item');
assert.strictEqual(configNavigatorItems.length, data?.length ?? 0);
assert.strictEqual(configNavigatorItems.length > 0, true);
});
});
Loading