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