diff --git a/salt/metalk8s/addons/ui/deployed/ui.sls.in b/salt/metalk8s/addons/ui/deployed/ui.sls.in
index 4ff03832a5..d62c12d992 100644
--- a/salt/metalk8s/addons/ui/deployed/ui.sls.in
+++ b/salt/metalk8s/addons/ui/deployed/ui.sls.in
@@ -68,6 +68,15 @@ Create shell-ui ConfigMap:
"clientId": "metalk8s-ui",
"responseType": "id_token",
"scopes": "openid profile email groups offline_access audience:server:client_id:oidc-auth-client"
+ },
+ "options": {
+ "main": {
+ "https://{{ ingress_control_plane }}/":{ "en": "Platform", "fr": "Plateforme" },
+ "https://{{ ingress_control_plane }}/alerts":{ "en": "Alerts", "fr": "Alertes" }
+ },
+ "subLogin": {
+ "https://{{ ingress_control_plane }}/docs":{ "en": "Documentation", "fr": "Documentation" }
+ }
}
}
diff --git a/shell-ui/package-lock.json b/shell-ui/package-lock.json
index 02e4d33063..495ae465cc 100644
--- a/shell-ui/package-lock.json
+++ b/shell-ui/package-lock.json
@@ -1898,8 +1898,8 @@
"dev": true
},
"@scality/core-ui": {
- "version": "git+https://github.com/scality/core-ui.git#3c6bbba93f1bd14b113abfc02be74ee3d2c2690f",
- "from": "@scality/core-ui@github:scality/core-ui.git#v0.7.1"
+ "version": "github:scality/core-ui#065a06c08c7a4f4d0c8f0fe74f722ffdc23d7762",
+ "from": "github:scality/core-ui#v0.13.0"
},
"@scarf/scarf": {
"version": "1.1.0",
@@ -9651,6 +9651,15 @@
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
+ "quicklink": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/quicklink/-/quicklink-2.1.0.tgz",
+ "integrity": "sha512-1K4ekaequXoC1m+gbYiEZNL+nMRfZBSNF/lxqsZSgZuAMekZXN1Or5s8d+XtpY9SurnuateLv1VPZviZg5MeDw==",
+ "requires": {
+ "route-manifest": "^1.0.0",
+ "throttles": "^1.0.0"
+ }
+ },
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -9960,6 +9969,11 @@
"define-properties": "^1.1.3"
}
},
+ "regexparam": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-1.3.0.tgz",
+ "integrity": "sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g=="
+ },
"regexpu-core": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz",
@@ -10266,6 +10280,14 @@
"glob": "^7.1.3"
}
},
+ "route-manifest": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/route-manifest/-/route-manifest-1.0.0.tgz",
+ "integrity": "sha512-qn0xJr4nnF4caj0erOLLAHYiNyzqhzpUbgDQcEHrmBoG4sWCDLnIXLH7VccNSxe9cWgbP2Kw/OjME+eH3CeRSA==",
+ "requires": {
+ "regexparam": "^1.3.0"
+ }
+ },
"rsvp": {
"version": "4.8.5",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
@@ -11410,6 +11432,11 @@
"integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==",
"dev": true
},
+ "throttles": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/throttles/-/throttles-1.0.1.tgz",
+ "integrity": "sha512-fab7Xg+zELr9KOv4fkaBoe/b3L0GMGLd0IBSCn16GoE/Qx6/OfCr1eGNyEcDU2pUA79qQfZ8kPQWlRuok4YwTw=="
+ },
"thunky": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
diff --git a/shell-ui/package.json b/shell-ui/package.json
index d6ecd99e4e..b8fbb782c9 100644
--- a/shell-ui/package.json
+++ b/shell-ui/package.json
@@ -32,10 +32,11 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
- "@scality/core-ui": "github:scality/core-ui.git#v0.7.1",
+ "@scality/core-ui": "github:scality/core-ui.git#v0.13.0",
"oidc-client": "^1.11.3",
"oidc-react": "^1.1.5",
"polished": "^4.0.5",
+ "quicklink": "^2.1.0",
"react": "^17.0.1",
"react-debounce-input": "^3.2.3",
"react-dom": "^17.0.1",
diff --git a/shell-ui/public/shell/config.json b/shell-ui/public/shell/config.json
index 5c0d0fcf63..39375a6de8 100644
--- a/shell-ui/public/shell/config.json
+++ b/shell-ui/public/shell/config.json
@@ -6,5 +6,12 @@
"clientId": "metalk8s-ui",
"responseType": "id_token",
"scopes": "openid profile email groups offline_access audience:server:client_id:oidc-auth-client"
+ },
+ "options": {
+ "main": {
+ "http://localhost:8082/":{ "en": "Platform", "fr": "Plateforme" },
+ "http://localhost:8082/test":{ "en": "Test", "fr": "Test" }
+ },
+ "subLogin": {}
}
}
diff --git a/shell-ui/src/NavBar.js b/shell-ui/src/NavBar.js
index d53b7fc8ac..f71e6d761a 100644
--- a/shell-ui/src/NavBar.js
+++ b/shell-ui/src/NavBar.js
@@ -2,64 +2,101 @@
import CoreUINavbar from '@scality/core-ui/dist/components/navbar/Navbar.component';
import { useAuth } from 'oidc-react';
import { useLayoutEffect, useState } from 'react';
-import type { SolutionsNavbarProps } from './index';
+import type {
+ Options,
+ SolutionsNavbarProps,
+ TranslationAndGroups,
+} from './index';
import type { Node } from 'react';
import { logOut } from './auth/logout';
+import {
+ getAccessiblePathsFromOptions,
+ isEntryAccessibleByTheUser,
+ normalizePath,
+} from './auth/permissionUtils';
+import { prefetch } from 'quicklink';
-export const LoadingNavbar = (): Node => ;
+export const LoadingNavbar = (): Node => (
+
+);
-export const Navbar = ({
- options,
-}: {
- options: $PropertyType,
-}): Node => {
+const translateOptionsToMenu = (
+ options: Options,
+ section: 'main' | 'subLogin',
+ renderer: (
+ path: string,
+ translationAndGroup: TranslationAndGroups,
+ ) => { link: Node } | { label: string, onClick: () => void },
+ userGroups: string[],
+) => {
+ const normalizedLocation = normalizePath(location.href);
+ return (
+ Object.entries(options[section])
+ //$FlowIssue - flow typing for Object.entries incorrectly typing values as [string, mixed] instead of [string, TranslationAndGroups]
+ .filter((entry: [string, TranslationAndGroups]) =>
+ isEntryAccessibleByTheUser(entry, userGroups),
+ )
+ .map(
+ //$FlowIssue - flow typing for Object.entries incorrectly typing values as [string, mixed] instead of [string, TranslationAndGroups]
+ ([path, translationAndGroup]: [string, TranslationAndGroups], i) => {
+ try {
+ return {
+ ...renderer(path, translationAndGroup),
+ selected: translationAndGroup.activeIfMatches
+ ? new RegExp(translationAndGroup.activeIfMatches).test(
+ location.href,
+ )
+ : normalizedLocation === normalizePath(path),
+ };
+ } catch (e) {
+ throw new Error(
+ `[navbar][config] Invalid path specified in "options.${section}": "${path}" ` +
+ '(keys must be defined as fully qualified URLs, ' +
+ 'such as "{protocol}://{host}{path}?{queryParams}")',
+ );
+ }
+ },
+ )
+ );
+};
+
+export const Navbar = ({ options }: { options: Options }): Node => {
const auth = useAuth();
- const tabs = [
- {
- selected: true,
- title: 'Overview',
- onClick: () => console.log('Overview'),
- },
- ];
+ const userGroups: string[] = auth.userData?.profile?.groups || [];
+ const accessiblePaths = getAccessiblePathsFromOptions(options, userGroups);
+ useLayoutEffect(() => {
+ accessiblePaths.forEach((accessiblePath) => {
+ prefetch(accessiblePath);
+ });
+ }, [JSON.stringify(accessiblePaths)]);
+
+ const tabs = translateOptionsToMenu(
+ options,
+ 'main',
+ (path, translationAndGroup) => ({
+ link: {translationAndGroup.en},
+ }),
+ userGroups,
+ );
+
const rightActions = [
- {
- type: 'dropdown',
- text: 'FR',
- icon: ,
- items: [
- {
- label: 'English',
- name: 'EN',
- onClick: () => console.log('en'),
- },
- ],
- },
- {
- type: 'dropdown',
- icon: ,
- items: [
- { label: 'About', onClick: () => console.log('About clicked') },
- {
- label: 'Documentation',
- onClick: () => console.log('Documentation clicked'),
- },
- {
- label: 'Onboarding',
- onClick: () => console.log('Onboarding clicked'),
- },
- ],
- },
- {
- type: 'button',
- icon: ,
- onClick: () => console.log('Theme toggle clicked'),
- },
{
type: 'dropdown',
text: auth.userData?.profile.name || '',
icon: ,
items: [
+ ...translateOptionsToMenu(
+ options,
+ 'subLogin',
+ (path, translationAndGroup) => ({
+ label: translationAndGroup.en,
+ onClick: () => {
+ location.href = path;
+ },
+ }),
+ userGroups,
+ ),
{
label: 'Log out',
onClick: () => {
@@ -69,6 +106,7 @@ export const Navbar = ({
],
},
];
+
return (
);
diff --git a/shell-ui/src/Navbar.spec.js b/shell-ui/src/Navbar.spec.js
index 2c89c27787..1fe1997f3b 100644
--- a/shell-ui/src/Navbar.spec.js
+++ b/shell-ui/src/Navbar.spec.js
@@ -5,131 +5,233 @@ import './index';
import { waitForLoadingToFinish } from './__TESTS__/utils';
const server = setupServer(
- rest.get(
- 'https://mocked.ingress/oidc/.well-known/openid-configuration',
- (req, res, ctx) => {
- const result = {
- "issuer": "https://mocked.ingress/oidc",
- "authorization_endpoint": "https://mocked.ingress/oidc/auth",
- "token_endpoint": "https://mocked.ingress/oidc/token",
- "jwks_uri": "https://mocked.ingress/oidc/keys",
- "userinfo_endpoint": "https://mocked.ingress/oidc/userinfo",
- "response_types_supported": ["code", "id_token", "token"],
- "subject_types_supported": ["public"],
- "id_token_signing_alg_values_supported": ["RS256"],
- "scopes_supported": [
- "openid",
- "email",
- "groups",
- "profile",
- "offline_access"
- ],
- "token_endpoint_auth_methods_supported": ["client_secret_basic"],
- "claims_supported": [
- "aud",
- "email",
- "email_verified",
- "exp",
- "iat",
- "iss",
- "locale",
- "name",
- "sub"
- ]
- }
- ;
- return res(ctx.json(result));
- },
- )
-)
+ rest.get(
+ 'https://mocked.ingress/oidc/.well-known/openid-configuration',
+ (req, res, ctx) => {
+ const result = {
+ issuer: 'https://mocked.ingress/oidc',
+ authorization_endpoint: 'https://mocked.ingress/oidc/auth',
+ token_endpoint: 'https://mocked.ingress/oidc/token',
+ jwks_uri: 'https://mocked.ingress/oidc/keys',
+ userinfo_endpoint: 'https://mocked.ingress/oidc/userinfo',
+ response_types_supported: ['code', 'id_token', 'token'],
+ subject_types_supported: ['public'],
+ id_token_signing_alg_values_supported: ['RS256'],
+ scopes_supported: [
+ 'openid',
+ 'email',
+ 'groups',
+ 'profile',
+ 'offline_access',
+ ],
+ token_endpoint_auth_methods_supported: ['client_secret_basic'],
+ claims_supported: [
+ 'aud',
+ 'email',
+ 'email_verified',
+ 'exp',
+ 'iat',
+ 'iss',
+ 'locale',
+ 'name',
+ 'sub',
+ ],
+ };
+ return res(ctx.json(result));
+ },
+ ),
+);
+
+const mockOIDCProvider = () => {
+ // This is a hack to workarround the following issue : MSW return lower cased content-type header,
+ // oidc-client is internally using XMLHttpRequest to perform queries and retrieve response header Content-Type using 'XMLHttpRequest.prototype.getResponseHeader'.
+ // XMLHttpRequest.prototype.getResponseHeader is case sensitive and hence when receiving a response with header content-type it is not mapping it to Content-Type
+ const caseSensitiveGetResponseHeader =
+ XMLHttpRequest.prototype.getResponseHeader;
+ XMLHttpRequest.prototype.getResponseHeader = function (header) {
+ if (header === 'Content-Type') {
+ return caseSensitiveGetResponseHeader.call(this, 'content-type');
+ }
+ return caseSensitiveGetResponseHeader.call(this, header);
+ };
+};
describe('navbar', () => {
- // use fake timers to let react query retry immediately after promise failure
- jest.useFakeTimers();
-
- beforeAll(() => server.listen());
- afterEach(() => server.resetHandlers())
-
- it('should display a loading state when resolving its configuration', () => {
- //E
- render(
- )
- //V
- expect(screen.getByText('loading')).toBeInTheDocument();
- })
-
- it('should display an error state when it failed to resolves its configuration', async () => {
- //S
- server.use(
- rest.get(
- '/shell/config.json',
- (req, res, ctx) => {
- return res(ctx.status(500));
- },
- )
- );
-
- render(
- )
- //V
- await waitForLoadingToFinish();
-
- expect(screen.getByText(/Failed to load navbar configuration/i)).toBeInTheDocument();
- })
-
- it('should display expected menu when it resolved its configuration', async () => {
- //S
- server.use(
- rest.get(
- '/shell/config.json',
- (req, res, ctx) => {
- const result = {
-
- };
- return res(ctx.json(result));
- },
- ),
- )
-
- // This is a hack to workarround the following issue : MSW return lower cased content-type header,
- // oidc-client is internally using XMLHttpRequest to perform queries and retrieve response header Content-Type using 'XMLHttpRequest.prototype.getResponseHeader'.
- // XMLHttpRequest.prototype.getResponseHeader is case sensitive and hence when receiving a response with header content-type it is not mapping it to Content-Type
- const caseSensitiveGetResponseHeader = XMLHttpRequest.prototype.getResponseHeader;
- XMLHttpRequest.prototype.getResponseHeader = function(header) {
- if (header === 'Content-Type') {
- return caseSensitiveGetResponseHeader.call(this, 'content-type');
- }
- return caseSensitiveGetResponseHeader.call(this, header);
- }
-
- render(
- )
- //E
- await waitForLoadingToFinish();
-
- expect(screen.getByText(/overview/i)).toBeInTheDocument();
- })
-
- afterAll(() => server.close());
-})
+ // use fake timers to let react query retry immediately after promise failure
+ jest.useFakeTimers();
+
+ beforeAll(() => server.listen());
+ afterEach(() => server.resetHandlers());
+
+ it('should display a loading state when resolving its configuration', () => {
+ //E
+ render(
+ ,
+ );
+ //V
+ expect(screen.queryByText('loading')).toBeInTheDocument();
+ });
+
+ it('should display an error state when it failed to resolves its configuration', async () => {
+ //S
+ server.use(
+ rest.get('/shell/config.json', (req, res, ctx) => {
+ return res(ctx.status(500));
+ }),
+ );
+
+ render(
+ ,
+ );
+ //E
+ await waitForLoadingToFinish();
+ //V
+ expect(
+ screen.queryByText(/Failed to load navbar configuration/i),
+ ).toBeInTheDocument();
+ });
+
+ it('should display expected selected menu when it matches by exact loaction (default behavior)', async () => {
+ //S
+ mockOIDCProvider();
+ //E
+ render(
+ ,
+ );
+ await waitForLoadingToFinish();
+ //V
+ const platformEntry = screen.getByRole('tab', {
+ name: /Platform/i,
+ selected: true,
+ });
+ expect(platformEntry).toBeInTheDocument();
+ });
+
+ it('should display expected selected menu when it matches by regex', async () => {
+ //S
+ mockOIDCProvider();
+ //E
+ render(
+ ,
+ );
+ await waitForLoadingToFinish();
+ //V
+ const platformEntry = screen.getByRole('tab', {
+ name: /Platform/i,
+ selected: true,
+ });
+ expect(platformEntry).toBeInTheDocument();
+ });
+
+ it('should display expected menu when it resolved its configuration', async () => {
+ //S
+ server.use(
+ rest.get('/shell/config.json', (req, res, ctx) => {
+ const result = {};
+ return res(ctx.json(result));
+ }),
+ );
+
+ mockOIDCProvider();
+
+ render(
+ ,
+ );
+ //E
+ await waitForLoadingToFinish();
+ //V
+ expect(screen.getByText(/Platform/i)).toBeInTheDocument();
+ });
+
+ it('should not display a restrained menu when an user is not authorized', async () => {
+ //S
+
+ mockOIDCProvider();
+
+ render(
+ ,
+ );
+ //E
+ await waitForLoadingToFinish();
+ //V
+ expect(screen.queryByText(/Platform/i)).toBeInTheDocument();
+ expect(screen.queryByText(/Test/i)).not.toBeInTheDocument();
+ });
+
+ afterAll(() => server.close());
+});
diff --git a/shell-ui/src/auth/permissionUtils.js b/shell-ui/src/auth/permissionUtils.js
new file mode 100644
index 0000000000..4d152babcf
--- /dev/null
+++ b/shell-ui/src/auth/permissionUtils.js
@@ -0,0 +1,41 @@
+//@flow
+import type { Options, TranslationAndGroups } from '../index';
+
+export const isEntryAccessibleByTheUser = (
+ [path, translationAndGroup]: [string, TranslationAndGroups],
+ userGroups: string[],
+): boolean => {
+ return (
+ translationAndGroup.groups?.every((group) => userGroups.includes(group)) ??
+ true
+ );
+};
+
+export const getAccessiblePathsFromOptions = (
+ options: Options,
+ userGroups: string[],
+): string[] => {
+ return (
+ [...Object.entries(options.main), ...Object.entries(options.subLogin)]
+ //$FlowIssue - flow typing for Object.entries incorrectly typing values as [string, mixed] instead of [string, TranslationAndGroups]
+ .filter((entry: [string, TranslationAndGroups]) =>
+ isEntryAccessibleByTheUser(entry, userGroups),
+ )
+ .map(([path]) => path)
+ );
+};
+
+export const normalizePath = (path: string): string => {
+ const url = new URL(path);
+ return url.origin + url.pathname;
+};
+
+export const isPathAccessible = (
+ path: string,
+ accessiblePaths: string[],
+): boolean => {
+ const normalizedPath = normalizePath(path);
+ return accessiblePaths.some(
+ (accessiblePath) => normalizePath(accessiblePath) === normalizedPath,
+ );
+};
diff --git a/shell-ui/src/auth/permissionUtils.spec.js b/shell-ui/src/auth/permissionUtils.spec.js
new file mode 100644
index 0000000000..8652e74660
--- /dev/null
+++ b/shell-ui/src/auth/permissionUtils.spec.js
@@ -0,0 +1,118 @@
+//@flow
+import {
+ getAccessiblePathsFromOptions,
+ isEntryAccessibleByTheUser,
+ isPathAccessible,
+ normalizePath,
+} from './permissionUtils';
+import { type Options } from '../index';
+
+describe('permission utils - isEntryAccessibleByTheUser', () => {
+ it('should return true if the user has explicit access', () => {
+ //E
+ const hasAccess = isEntryAccessibleByTheUser(
+ ['http://fake/path', { en: 'Path', fr: 'Path', groups: ['group'] }],
+ ['group'],
+ );
+ //V
+ expect(hasAccess).toBe(true);
+ });
+ it('should return true if the path is public', () => {
+ //E
+ const hasAccess = isEntryAccessibleByTheUser(
+ ['http://fake/path', { en: 'Path', fr: 'Path' }],
+ ['group'],
+ );
+ //V
+ expect(hasAccess).toBe(true);
+ });
+ it('should return false if the user is not part of the expected group', () => {
+ //E
+ const hasAccess = isEntryAccessibleByTheUser(
+ ['http://fake/path', { en: 'Path', fr: 'Path', groups: ['group'] }],
+ [],
+ );
+ //V
+ expect(hasAccess).toBe(false);
+ });
+});
+
+describe('permission utils - getAccessiblePathsFromOptions', () => {
+ it('should return accessible paths', () => {
+ //S
+ const options: Options = {
+ main: {
+ 'http://fake/publicPath': { en: 'publicPath', fr: 'publicPath' },
+ 'http://fake/groupPath': {
+ en: 'groupPath',
+ fr: 'groupPath',
+ groups: ['group'],
+ },
+ 'http://fake/groupAPath': {
+ en: 'groupAPath',
+ fr: 'groupAPath',
+ groups: ['groupA'],
+ },
+ },
+ subLogin: {
+ 'http://fake/subLoginPublicPath': {
+ en: 'publicPath',
+ fr: 'publicPath',
+ },
+ 'http://fake/subLoginGroupPath': {
+ en: 'groupPath',
+ fr: 'groupPath',
+ groups: ['group'],
+ },
+ 'http://fake/subLoginGroupAPath': {
+ en: 'groupAPath',
+ fr: 'groupAPath',
+ groups: ['groupA'],
+ },
+ },
+ };
+ //E
+ const accessiblePaths = getAccessiblePathsFromOptions(options, ['group']);
+ //V
+ expect(accessiblePaths).toEqual([
+ 'http://fake/publicPath',
+ 'http://fake/groupPath',
+ 'http://fake/subLoginPublicPath',
+ 'http://fake/subLoginGroupPath',
+ ]);
+ });
+});
+
+describe('permission utils - normalizePath', () => {
+ it('should normalize path', () => {
+ expect(normalizePath('http://fake/groupPath')).toEqual(
+ 'http://fake/groupPath',
+ );
+ expect(normalizePath('http://fake/path/subPath?a=test#test')).toEqual(
+ 'http://fake/path/subPath',
+ );
+ });
+
+ it('should throw if the path is an invalid url', () => {
+ expect(() => normalizePath('invalidUrl')).toThrow();
+ });
+});
+
+describe('permission utils - isPathAccessible', () => {
+ it('should return true when the path is accessible', () => {
+ //E
+ const isAccessible = isPathAccessible('http://fake/path?hello=world', [
+ 'http://fake/path?hello=test',
+ ]);
+ //V
+ expect(isAccessible).toBe(true);
+ });
+ it('should return false when the path is not accessible', () => {
+ //E
+ const isAccessible = isPathAccessible('http://fake/path?hello=world', [
+ 'http://fake/another-path?hello=test',
+ ]);
+ //V
+ expect(isAccessible).toBe(false);
+ });
+});
diff --git a/shell-ui/src/index.js b/shell-ui/src/index.js
index 792b154887..1dbefbad03 100644
--- a/shell-ui/src/index.js
+++ b/shell-ui/src/index.js
@@ -10,11 +10,16 @@ import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import { LoadingNavbar, Navbar } from './NavBar';
import { UserDataListener } from './UserDataListener';
import { logOut } from './auth/logout';
+import { prefetch } from "quicklink";
+import {defaultTheme} from '@scality/core-ui/dist/style/theme';
const EVENTS_PREFIX = 'solutions-navbar--';
export const AUTHENTICATED_EVENT: string = EVENTS_PREFIX + 'authenticated';
-type Options = { [path: string]: { en: string, fr: string, roles?: string[] } }; // TODO should be able to accept configs for paths in dropdown menu under user name
+export type TranslationAndGroups = { en: string, fr: string, groups?: string[], activeIfMatches?: string };
+export type MenuItems = {[path: string]: TranslationAndGroups }
+
+export type Options = { main: MenuItems, subLogin: MenuItems };
export type SolutionsNavbarProps = {
'oidc-provider-url'?: string,
@@ -23,7 +28,7 @@ export type SolutionsNavbarProps = {
'response-type'?: string,
'redirect-url'?: string,
'config-url'?: string,
- options?: Options,
+ options?: string,
onAuthenticated?: (evt: CustomEvent) => void,
logOut?: () => void,
setUserManager?: (userManager: UserManager) => void,
@@ -67,6 +72,13 @@ const SolutionsNavbar = ({
},
);
+ useLayoutEffect(() => {
+ const savedRedirectUri = localStorage.getItem('redirectUrl');
+ if (savedRedirectUri) {
+ prefetch(savedRedirectUri)
+ }
+ }, []);
+
switch (status) {
case 'idle':
case 'loading':
@@ -92,6 +104,8 @@ const SolutionsNavbar = ({
userStore: new WebStorageStateStore({ store: localStorage }),
});
+ const computedMenuOptions = options ? JSON.parse(options) : config.options || { main: {}, subLogin: {} };
+
if (setUserManager) {
setUserManager(userManager);
}
@@ -102,6 +116,7 @@ const SolutionsNavbar = ({
},
onSignIn: () => {
const savedRedirectUri = localStorage.getItem('redirectUrl');
+ localStorage.removeItem('redirectUrl');
if (savedRedirectUri) {
location.href = savedRedirectUri;
} else {
@@ -122,34 +137,11 @@ const SolutionsNavbar = ({
-
+
);
@@ -164,7 +156,7 @@ SolutionsNavbar.propTypes = {
'config-url': PropTypes.string,
'redirect-url': PropTypes.string,
'response-type': PropTypes.string,
- options: PropTypes.object,
+ options: PropTypes.string,
};
const SolutionsNavbarProviderWrapper = (props: SolutionsNavbarProps) => {
diff --git a/shell-ui/src/index.mdx b/shell-ui/src/index.mdx
index ca62ac2720..373e919a23 100644
--- a/shell-ui/src/index.mdx
+++ b/shell-ui/src/index.mdx
@@ -8,4 +8,13 @@ import * as WebComponent from './index.js';
response-type="id_token"
redirect-url="http://localhost:8082"
scopes="openid profile email groups offline_access audience:server:client_id:oidc-auth-client"
+ options={
+ JSON.stringify({
+ "main": {
+ "http://localhost:8082/":{ "en": "Platform", "fr": "Plateforme" },
+ "http://localhost:8082/test":{ "en": "Test", "fr": "Test" }
+ },
+ "subLogin": {}
+ })
+ }
/>
diff --git a/shell-ui/src/setupTests.js b/shell-ui/src/setupTests.js
index baa6eba120..184a9e4f53 100644
--- a/shell-ui/src/setupTests.js
+++ b/shell-ui/src/setupTests.js
@@ -1,10 +1,10 @@
import 'whatwg-fetch';
-import "regenerator-runtime/runtime"
+import 'regenerator-runtime/runtime';
import '@testing-library/jest-dom/extend-expect';
const nodeCrypto = require('crypto');
window.crypto = {
getRandomValues: function (buffer) {
return nodeCrypto.randomFillSync(buffer);
- }
+ },
};
diff --git a/ui/cypress/fixtures/shell-config.json b/ui/cypress/fixtures/shell-config.json
index cabdcd3dcb..791da42d6e 100644
--- a/ui/cypress/fixtures/shell-config.json
+++ b/ui/cypress/fixtures/shell-config.json
@@ -6,6 +6,15 @@
"clientId": "metalk8s-ui",
"responseType": "id_token",
"scopes": "openid profile email groups offline_access audience:server:client_id:oidc-auth-client"
+ },
+ "options": {
+ "main": {
+ "https://mocked.ingress/":{ "en": "Platform", "fr": "Plateforme" },
+ "https://mocked.ingress/alerts":{ "en": "Alerts", "fr": "Alertes" }
+ },
+ "subLogin": {
+ "https://mocked.ingress/docs":{ "en": "Documentation", "fr": "Documentation" }
+ }
}
}
\ No newline at end of file