diff --git a/.storybook/__snapshots__/Welcome.story.storyshot b/.storybook/__snapshots__/Welcome.story.storyshot index 44edbaf72e..4095e27f37 100644 --- a/.storybook/__snapshots__/Welcome.story.storyshot +++ b/.storybook/__snapshots__/Welcome.story.storyshot @@ -3024,6 +3024,18 @@ exports[`Storybook Snapshot tests and console checks Storyshots 0/Getting Starte useSuiteHeaderData +
+
+
+ suiteHeaderData +
+
diff --git a/jest.config.js b/jest.config.js index 0a76eec30b..501c4af416 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,7 @@ module.exports = { 'src/components/**/*.js?(x)', '!src/**/*.story.js?(x)', '!src/**/hooks/*.js', + '!src/components/SuiteHeader/util/suiteHeaderData.js', ], coveragePathIgnorePatterns: ['/node_modules/', '/lib/', '/coverage/'], coverageReporters: ['html', 'text-summary', 'lcov'], diff --git a/src/components/SuiteHeader/SuiteHeader.story.jsx b/src/components/SuiteHeader/SuiteHeader.story.jsx index f622bf0cfc..e4ec6d89bf 100644 --- a/src/components/SuiteHeader/SuiteHeader.story.jsx +++ b/src/components/SuiteHeader/SuiteHeader.story.jsx @@ -7,6 +7,7 @@ import Group from '@carbon/icons-react/lib/group/24'; import SuiteHeader from './SuiteHeader'; import SuiteHeaderI18N from './i18n'; +// import getSuiteHeaderData from './util/suiteHeaderData'; // import useSuiteHeaderData from './hooks/useSuiteHeaderData'; const sideNavLinks = [ @@ -227,6 +228,109 @@ export const HeaderWithSurveyNotification = () => { ); }; +/* Sample of SuiteHeader usage with data hook +export const HeaderWithHook = () => { + const StatefulExample = () => { + const [data] = useSuiteHeaderData({ + // baseApiUrl: 'http://localhost:3001/internal', + domain: 'mydomain.com', + isTest: true, + surveyConfig: { + id: 'suite', + delayIntervalDays: 30, + frequencyDays: 90, + }, + lang: 'en', + }); + const surveyData = data.showSurvey + ? { + surveyLink: 'https://www.ibm.com', + privacyLink: 'https://www.ibm.com', + } + : null; + return data.username ? ( + + ) : null; + }; + return ; +}; + +HeaderWithHook.story = { + name: 'Header with hook', +}; + +export const HeaderWithDataFetching = () => { + const StatefulExample = () => { + const [data, setData] = useState({ + username: null, + userDisplayName: null, + email: null, + routes: { + profile: null, + navigator: null, + admin: null, + logout: null, + about: null, + documentation: null, + whatsNew: null, + requestEnhancement: null, + support: null, + gettingStarted: null, + }, + applications: [], + showSurvey: false, + }); + useEffect(() => { + getSuiteHeaderData({ + // baseApiUrl: 'http://localhost:3001/internal', + domain: 'mydomain.com', + isTest: true, + surveyConfig: { + id: 'suite', + delayIntervalDays: 30, + frequencyDays: 90, + }, + lang: 'en', + }).then((suiteHeaderData) => setData(suiteHeaderData)); + }, []); + + const surveyData = data.showSurvey + ? { + surveyLink: 'https://www.ibm.com', + privacyLink: 'https://www.ibm.com', + } + : null; + return data.username ? ( + + ) : null; + }; + return ; +}; + +HeaderWithDataFetching.story = { + name: 'Header with data fetching', +}; + +*/ + HeaderWithSurveyNotification.story = { name: 'Header with survey notification', }; diff --git a/src/components/SuiteHeader/hooks/useSuiteHeaderData.js b/src/components/SuiteHeader/hooks/useSuiteHeaderData.js index 2f3414a310..d4139f61bc 100644 --- a/src/components/SuiteHeader/hooks/useSuiteHeaderData.js +++ b/src/components/SuiteHeader/hooks/useSuiteHeaderData.js @@ -1,168 +1,12 @@ import { useState, useEffect, useCallback } from 'react'; -import moment from 'moment'; - -import SuiteHeaderI18N from '../i18n'; // eslint-disable-next-line import/extensions -import testApiData from './suiteHeaderData.fixture.js'; - -// default route calculation logic -const calcRoutes = (domain, user, workspaces, applications) => { - const workspaceId = Object.keys(user.workspaces)[0]; - const getApplicationUrl = (appId) => - user.workspaces[workspaceId].applications[appId].href; - const isAdmin = user.permissions.systemAdmin || user.permissions.userAdmin; - const routeData = { - profile: `https://home.${domain}/myaccount`, - navigator: `https://${workspaceId}.home.${domain}`, - admin: isAdmin ? `https://admin.${domain}` : null, - logout: `https://home.${domain}/logout`, - whatsNew: - 'https://www.ibm.com/support/knowledgecenter/SSRHPA_current/appsuite/overview/whats_new.html', - gettingStarted: - 'https://www.ibm.com/support/knowledgecenter/SSRHPA_current/appsuite/overview/getting_started.html', - documentation: 'https://www.ibm.com/support/knowledgecenter/SSRHPA_current', - requestEnhancement: 'https://ibm-watson-iot.ideas.aha.io/', - support: 'https://www.ibm.com/mysupport', - about: `https://home.${domain}/about`, - }; - const appOrdering = ['monitor', 'health', 'predict', 'visualinspection']; - const workspaceApplications = user.workspaces[workspaceId].applications || {}; - const applicationSyncStates = user.applications || {}; - const appData = Object.keys(workspaceApplications) - .filter((appId) => workspaceApplications[appId].role !== 'NO_ACCESS') - .filter((appId) => applicationSyncStates[appId]?.sync?.state === 'SUCCESS') - .filter( - (appId) => - (applications.find((i) => i.id === appId) || {}).category === - 'application' - ) - .sort( - (a, b) => - appOrdering.findIndex((i) => i === a) - - appOrdering.findIndex((i) => i === b) - ) - .map((appId) => ({ - id: appId, - name: - (applications.find((i) => i.id === appId) || {}).name || - appId.charAt(0).toUpperCase() + appId.slice(1), - href: getApplicationUrl(appId), - isExternal: getApplicationUrl(appId).indexOf(domain) >= 0, - })) - .sort(); - return [routeData, appData]; -}; - -// Default survey status calculation logic -const calcSurveyStatus = async (userId, surveyConfig, apiFct) => { - let showSurvey = false; - - // Check if it is time to show the survey - const isTimeForSurvey = (surveyData) => { - // If survey is not enabled, return false - if (!surveyData.enabled) { - return false; - } - // If lastPromptTimestamp is set and it is greater than initialInteractionTimestamp, - // it means that at least one survey has already been prompted to the user, - // so, we check if another survey prompt is due. - if ( - surveyData.lastPromptTimestamp && - moment(surveyData.lastPromptTimestamp).isAfter( - surveyData.initialInteractionTimestamp - ) - ) { - if ( - moment().diff(surveyData.lastPromptTimestamp, 'days') > - surveyData.frequencyDays - ) { - return true; - } - } - // No survey has been prompted yet, so we check if it is time for the first one. - else if ( - moment().diff(surveyData.initialInteractionTimestamp, 'days') > - surveyData.delayIntervalDays - ) { - return true; - } - return false; - }; - - const surveyData = await apiFct( - 'GET', - `/users/${userId}/surveys/${surveyConfig.id}` - ); - if (surveyData) { - // Survey data found, check it some config props need to be updated on the backend - const updateObject = {}; - ['delayIntervalDays', 'frequencyDays', 'enabled'].forEach((surveyProp) => { - if ( - surveyConfig[surveyProp] && - surveyConfig[surveyProp] !== surveyData[surveyProp] - ) { - updateObject[surveyProp] = surveyConfig[surveyProp]; - } - }); - // If at least one config prop is different than the one in the existing record, update it - if (Object.keys(updateObject).length > 0) { - await apiFct( - 'PUT', - `/users/${userId}/surveys/${surveyConfig.id}`, - updateObject - ); - } - // Based on survey data and current timestamp, make the proper time comparisons to check if it is time to show a survey - showSurvey = isTimeForSurvey(surveyData); - if (showSurvey) { - // Update lastPromptTimestamp to the current timestamp so that we need to wait another 'frequencyDays' days until the next survey - await apiFct('PUT', `/users/${userId}/surveys/${surveyConfig.id}`, { - lastPromptTimestamp: moment.utc().format(), - }); - } - } else { - // Survey record not found, create it - await apiFct('POST', `/users/${userId}/surveys`, { - ...surveyConfig, - delayIntervalDays: surveyConfig.delayIntervalDays ?? 30, - frequencyDays: surveyConfig.frequencyDays ?? 90, - enabled: surveyConfig.enabled ?? true, - initialInteractionTimestamp: moment.utc().format(), - }); - } - return showSurvey; -}; - -// default i18n calculation logic -const calcI18N = (i18nData) => ({ - ...i18nData, - surveyTitle: (solutionName) => - i18nData.surveyTitle.replace('{solutionName}', solutionName), - profileLogoutModalBody: (solutionName, userName) => - i18nData.profileLogoutModalBody - .replace('{solutionName}', solutionName) - .replace('{userName}', userName), -}); - -const defaultFetchApi = async (method, url, body, headers, testResponse) => - testResponse || - fetch(url, { - method, - credentials: 'include', - headers: headers || { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }) - .then((res) => res.json()) - .then((resJson) => { - // Don't return any data if an error happened (401, 404, 409, etc) - if (resJson.error || resJson.exception) { - return null; - } - return resJson; - }); +import getSuiteHeaderData, { + calcRoutes, + calcSurveyStatus, + calcI18N, + defaultFetchApi, +} from '../util/suiteHeaderData'; const useSuiteHeaderData = ({ baseApiUrl, @@ -194,67 +38,24 @@ const useSuiteHeaderData = ({ gettingStarted: null, }, applications: [], + showSurvey: false, }); const refreshData = useCallback(async () => { - const api = (method, path, body, headers) => - fetchApi( - method, - `${baseApiUrl}${path}`, - body, - headers, - isTest ? testApiData[path] : null - ); - try { setIsLoading(true); - const profileData = await api('GET', '/profile'); - const appsData = await api('GET', '/applications'); - const eamData = await api('GET', '/config/eam'); - const i18nData = await api('GET', `/i18n/header/${isTest ? 'en' : lang}`); - - // Routes - const [routes, applications] = calculateRoutes( + const suiteHeaderData = await getSuiteHeaderData({ + baseApiUrl, domain, - profileData.user, - profileData.workspaces, - appsData - ); - - // Survey - const showSurvey = surveyConfig?.id - ? await calculateSurveyStatus( - profileData.user.username, - surveyConfig, - api - ) - : false; - - // i18n - const i18n = i18nData ? calculateI18N(i18nData) : SuiteHeaderI18N.en; - - setData({ - username: profileData.user.username, - userDisplayName: profileData.user.displayName, - email: profileData.user.email, - routes, - applications: [ - ...(eamData?.url - ? [ - { - id: 'eam', - name: 'Manage', - href: eamData.url, - isExternal: true, - }, - ] - : []), - ...applications, - ], - i18n, - showSurvey, + lang, + calculateRoutes, + calculateSurveyStatus, + calculateI18N, + fetchApi, + surveyConfig, + isTest, }); - setIsLoading(false); + setData(suiteHeaderData); } catch (err) { setError(err); setIsLoading(false); diff --git a/src/components/SuiteHeader/hooks/suiteHeaderData.fixture.js b/src/components/SuiteHeader/util/suiteHeaderData.fixture.js similarity index 100% rename from src/components/SuiteHeader/hooks/suiteHeaderData.fixture.js rename to src/components/SuiteHeader/util/suiteHeaderData.fixture.js diff --git a/src/components/SuiteHeader/util/suiteHeaderData.js b/src/components/SuiteHeader/util/suiteHeaderData.js new file mode 100644 index 0000000000..d01c0e2fe1 --- /dev/null +++ b/src/components/SuiteHeader/util/suiteHeaderData.js @@ -0,0 +1,236 @@ +import moment from 'moment'; + +import SuiteHeaderI18N from '../i18n'; + +// eslint-disable-next-line import/extensions +import testApiData from './suiteHeaderData.fixture.js'; + +// default route calculation logic +export const calcRoutes = (domain, user, workspaces, applications) => { + const workspaceId = Object.keys(user.workspaces)[0]; + const getApplicationUrl = (appId) => + user.workspaces[workspaceId].applications[appId].href; + const isAdmin = user.permissions.systemAdmin || user.permissions.userAdmin; + const routeData = { + profile: `https://home.${domain}/myaccount`, + navigator: `https://${workspaceId}.home.${domain}`, + admin: isAdmin ? `https://admin.${domain}` : null, + logout: `https://home.${domain}/logout`, + whatsNew: + 'https://www.ibm.com/support/knowledgecenter/SSRHPA_current/appsuite/overview/whats_new.html', + gettingStarted: + 'https://www.ibm.com/support/knowledgecenter/SSRHPA_current/appsuite/overview/getting_started.html', + documentation: 'https://www.ibm.com/support/knowledgecenter/SSRHPA_current', + requestEnhancement: 'https://ibm-watson-iot.ideas.aha.io/', + support: 'https://www.ibm.com/mysupport', + about: `https://home.${domain}/about`, + }; + const appOrdering = ['monitor', 'health', 'predict', 'visualinspection']; + const workspaceApplications = user.workspaces[workspaceId].applications || {}; + const applicationSyncStates = user.applications || {}; + const appData = Object.keys(workspaceApplications) + .filter((appId) => workspaceApplications[appId].role !== 'NO_ACCESS') + .filter((appId) => applicationSyncStates[appId]?.sync?.state === 'SUCCESS') + .filter( + (appId) => + (applications.find((i) => i.id === appId) || {}).category === + 'application' + ) + .sort( + (a, b) => + appOrdering.findIndex((i) => i === a) - + appOrdering.findIndex((i) => i === b) + ) + .map((appId) => ({ + id: appId, + name: + (applications.find((i) => i.id === appId) || {}).name || + appId.charAt(0).toUpperCase() + appId.slice(1), + href: getApplicationUrl(appId), + isExternal: getApplicationUrl(appId).indexOf(domain) >= 0, + })) + .sort(); + return [routeData, appData]; +}; + +// Default survey status calculation logic +export const calcSurveyStatus = async (userId, surveyConfig, apiFct) => { + let showSurvey = false; + + // Check if it is time to show the survey + const isTimeForSurvey = (surveyData) => { + // If survey is not enabled, return false + if (!surveyData.enabled) { + return false; + } + // If lastPromptTimestamp is set and it is greater than initialInteractionTimestamp, + // it means that at least one survey has already been prompted to the user, + // so, we check if another survey prompt is due. + if ( + surveyData.lastPromptTimestamp && + moment(surveyData.lastPromptTimestamp).isAfter( + surveyData.initialInteractionTimestamp + ) + ) { + if ( + moment().diff(surveyData.lastPromptTimestamp, 'days') > + surveyData.frequencyDays + ) { + return true; + } + } + // No survey has been prompted yet, so we check if it is time for the first one. + else if ( + moment().diff(surveyData.initialInteractionTimestamp, 'days') > + surveyData.delayIntervalDays + ) { + return true; + } + return false; + }; + + const surveyData = await apiFct( + 'GET', + `/users/${userId}/surveys/${surveyConfig.id}` + ); + if (surveyData) { + // Survey data found, check it some config props need to be updated on the backend + const updateObject = {}; + ['delayIntervalDays', 'frequencyDays', 'enabled'].forEach((surveyProp) => { + if ( + surveyConfig[surveyProp] && + surveyConfig[surveyProp] !== surveyData[surveyProp] + ) { + updateObject[surveyProp] = surveyConfig[surveyProp]; + } + }); + // If at least one config prop is different than the one in the existing record, update it + if (Object.keys(updateObject).length > 0) { + await apiFct( + 'PUT', + `/users/${userId}/surveys/${surveyConfig.id}`, + updateObject + ); + } + // Based on survey data and current timestamp, make the proper time comparisons to check if it is time to show a survey + showSurvey = isTimeForSurvey(surveyData); + if (showSurvey) { + // Update lastPromptTimestamp to the current timestamp so that we need to wait another 'frequencyDays' days until the next survey + await apiFct('PUT', `/users/${userId}/surveys/${surveyConfig.id}`, { + lastPromptTimestamp: moment.utc().format(), + }); + } + } else { + // Survey record not found, create it + await apiFct('POST', `/users/${userId}/surveys`, { + ...surveyConfig, + delayIntervalDays: surveyConfig.delayIntervalDays ?? 30, + frequencyDays: surveyConfig.frequencyDays ?? 90, + enabled: surveyConfig.enabled ?? true, + initialInteractionTimestamp: moment.utc().format(), + }); + } + return showSurvey; +}; + +// default i18n calculation logic +export const calcI18N = (i18nData) => ({ + ...i18nData, + surveyTitle: (solutionName) => + i18nData.surveyTitle.replace('{solutionName}', solutionName), + profileLogoutModalBody: (solutionName, userName) => + i18nData.profileLogoutModalBody + .replace('{solutionName}', solutionName) + .replace('{userName}', userName), +}); + +export const defaultFetchApi = async ( + method, + url, + body, + headers, + testResponse +) => + testResponse || + fetch(url, { + method, + credentials: 'include', + headers: headers || { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + .then((res) => res.json()) + .then((resJson) => { + // Don't return any data if an error happened (401, 404, 409, etc) + if (resJson.error || resJson.exception) { + return null; + } + return resJson; + }); + +const getSuiteHeaderData = async ({ + baseApiUrl, + domain, + lang = 'en', + calculateRoutes = calcRoutes, + calculateSurveyStatus = calcSurveyStatus, + calculateI18N = calcI18N, + fetchApi = defaultFetchApi, + surveyConfig = null, + isTest = false, +}) => { + const api = (method, path, body, headers) => + fetchApi( + method, + `${baseApiUrl}${path}`, + body, + headers, + isTest ? testApiData[path] : null + ); + + const profileData = await api('GET', '/profile'); + const appsData = await api('GET', '/applications'); + const eamData = await api('GET', '/config/eam'); + const i18nData = await api('GET', `/i18n/header/${isTest ? 'en' : lang}`); + + // Routes + const [routes, applications] = calculateRoutes( + domain, + profileData.user, + profileData.workspaces, + appsData + ); + + // Survey + const showSurvey = surveyConfig?.id + ? await calculateSurveyStatus(profileData.user.username, surveyConfig, api) + : false; + + // i18n + const i18n = i18nData ? calculateI18N(i18nData) : SuiteHeaderI18N.en; + + return { + username: profileData.user.username, + userDisplayName: profileData.user.displayName, + email: profileData.user.email, + routes, + applications: [ + ...(eamData?.url + ? [ + { + id: 'eam', + name: 'Manage', + href: eamData.url, + isExternal: true, + }, + ] + : []), + ...applications, + ], + i18n, + showSurvey, + }; +}; + +export default getSuiteHeaderData; diff --git a/src/index.js b/src/index.js index b3c810f435..a6f8b6b59a 100755 --- a/src/index.js +++ b/src/index.js @@ -59,6 +59,7 @@ export SuiteHeaderAppSwitcher from './components/SuiteHeader/SuiteHeaderAppSwitc export SuiteHeaderLogoutModal from './components/SuiteHeader/SuiteHeaderLogoutModal/SuiteHeaderLogoutModal'; export SuiteHeaderI18N from './components/SuiteHeader/i18n'; export useSuiteHeaderData from './components/SuiteHeader/hooks/useSuiteHeaderData'; +export suiteHeaderData from './components/SuiteHeader/util/suiteHeaderData'; // Dashboard export Dashboard from './components/Dashboard/Dashboard'; diff --git a/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap b/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap index d66d0b3eed..b52404c173 100644 --- a/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap +++ b/src/utils/__tests__/__snapshots__/publicAPI.test.js.snap @@ -7122,6 +7122,7 @@ Map { }, }, "useSuiteHeaderData" => Object {}, + "suiteHeaderData" => Object {}, "Dashboard" => Object { "defaultProps": Object { "actions": Array [],