diff --git a/packages/manager/apps/sap-features-hub/README.md b/packages/manager/apps/sap-features-hub/README.md new file mode 100644 index 000000000000..7134b474488c --- /dev/null +++ b/packages/manager/apps/sap-features-hub/README.md @@ -0,0 +1,3 @@ +# @ovh-ux/manager-sap-features-hub-app + +> Manage SAP workloads in HostedPrivateCloud diff --git a/packages/manager/apps/sap-features-hub/index.html b/packages/manager/apps/sap-features-hub/index.html new file mode 100644 index 000000000000..ec4e92d05007 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/index.html @@ -0,0 +1,22 @@ + + + + + + + + OVHcloud + + + + + +
+ + + diff --git a/packages/manager/apps/sap-features-hub/package.json b/packages/manager/apps/sap-features-hub/package.json new file mode 100644 index 000000000000..eebd48f20ba8 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/package.json @@ -0,0 +1,60 @@ +{ + "name": "@ovh-ux/manager-sap-features-hub-app", + "version": "0.0.0", + "private": true, + "description": "Manage SAP workloads in HostedPrivateCloud", + "repository": { + "type": "git", + "url": "git+https://github.com/ovh/manager.git", + "directory": "packages/manager/apps/sap-features-hub" + }, + "license": "BSD-3-Clause", + "author": "OVH SAS", + "scripts": { + "build": "tsc && vite build", + "coverage": "vitest run --coverage", + "dev": "tsc && vite", + "start": "lerna exec --stream --scope='@ovh-ux/manager-sap-features-hub-app' --include-dependencies -- npm run build --if-present", + "start:dev": "lerna exec --stream --scope='@ovh-ux/manager-sap-features-hub-app' --include-dependencies -- npm run dev --if-present", + "start:watch": "lerna exec --stream --parallel --scope='@ovh-ux/manager-sap-features-hub-app' --include-dependencies -- npm run dev:watch --if-present", + "test": "vitest run" + }, + "dependencies": { + "@ovh-ux/manager-config": "*", + "@ovh-ux/manager-core-api": "*", + "@ovh-ux/manager-core-utils": "*", + "@ovh-ux/manager-react-components": "^2.2.0", + "@ovh-ux/manager-react-core-application": "*", + "@ovh-ux/manager-react-shell-client": "*", + "@ovh-ux/request-tagger": "*", + "@ovhcloud/ods-components": "^18.3.0", + "@ovhcloud/ods-themes": "^18.3.0", + "@tanstack/react-query": "^5.51.21", + "axios": "^1.1.2", + "clsx": "^1.2.1", + "i18next": "^23.8.2", + "i18next-http-backend": "^2.4.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.0.5", + "react-router-dom": "^6.3.0", + "tailwindcss": "^3.4.4" + }, + "devDependencies": { + "@ovh-ux/manager-vite-config": "*", + "@tanstack/react-query-devtools": "^5.51.21", + "@testing-library/dom": "^9.3.4", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.2.1", + "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^2.1.2", + "typescript": "^5.1.6", + "vite": "^5.2.13", + "vitest": "^2.1.2" + }, + "regions": [ + "CA", + "EU", + "US" + ] +} diff --git a/packages/manager/apps/sap-features-hub/postcss.config.js b/packages/manager/apps/sap-features-hub/postcss.config.js new file mode 100644 index 000000000000..12a703d900da --- /dev/null +++ b/packages/manager/apps/sap-features-hub/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/manager/apps/sap-features-hub/public/translations/dashboard/Messages_fr_FR.json b/packages/manager/apps/sap-features-hub/public/translations/dashboard/Messages_fr_FR.json new file mode 100644 index 000000000000..f42a27b3366a --- /dev/null +++ b/packages/manager/apps/sap-features-hub/public/translations/dashboard/Messages_fr_FR.json @@ -0,0 +1,7 @@ +{ + "title": "Dashboard page", + "error_service": "No services info", + "general_informations": "Informations générales", + "tab2": "Tab 2", + "back_link": "Retour à la liste" +} diff --git a/packages/manager/apps/sap-features-hub/public/translations/listing/Messages_fr_FR.json b/packages/manager/apps/sap-features-hub/public/translations/listing/Messages_fr_FR.json new file mode 100644 index 000000000000..c882bc92cfec --- /dev/null +++ b/packages/manager/apps/sap-features-hub/public/translations/listing/Messages_fr_FR.json @@ -0,0 +1,4 @@ +{ + "title": "Listing page", + "listing_resultats": "résultats" +} diff --git a/packages/manager/apps/sap-features-hub/public/translations/onboarding/Messages_fr_FR.json b/packages/manager/apps/sap-features-hub/public/translations/onboarding/Messages_fr_FR.json new file mode 100644 index 000000000000..fc039645a494 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/public/translations/onboarding/Messages_fr_FR.json @@ -0,0 +1,13 @@ +{ + "title": "sap-features-hub", + "description": "Découvrez des services de stockage managés qui s’appuient sur le système de fichiers OpenZFS. Bénéficiez en quelques clics d’espaces de stockage centralisés pour entreposer ou sauvegarder vos données et fichiers.", + "orderButtonLabel": "Commander un sap-features-hub", + "moreInfoButtonLabel": "En savoir plus sur sap-features-hub", + "guideCategory": "Tutoriel", + "guide1Title": "Premiers pas avec un sap-features-hub", + "guide1Description": "Découvrez comment gérer un NAS-HA depuis l'espace-client OVHcloud", + "guide2Title": "Monter votre NAS via un partage NFS", + "guide2Description": "Découvrez comment monter un NAS via un partage NFS", + "guide3Title": "Monter votre NAS sur Windows Server via CIFS", + "guide3Description": "Découvrez comment monter un NAS sur Windows Server via le protocole CIFS" +} diff --git a/packages/manager/apps/sap-features-hub/public/translations/sap-features-hub/Messages_fr_FR.json b/packages/manager/apps/sap-features-hub/public/translations/sap-features-hub/Messages_fr_FR.json new file mode 100644 index 000000000000..4f5119dbb5bf --- /dev/null +++ b/packages/manager/apps/sap-features-hub/public/translations/sap-features-hub/Messages_fr_FR.json @@ -0,0 +1,6 @@ +{ + "title": "Bienvenue uapp", + "crumb": "sap-features-hub", + "tabs_2": "Tabs 2", + "onboarding": "Onboarding" +} diff --git a/packages/manager/apps/sap-features-hub/public/translations/sap-features-hub/error/Messages_fr_FR.json b/packages/manager/apps/sap-features-hub/public/translations/sap-features-hub/error/Messages_fr_FR.json new file mode 100644 index 000000000000..2c575c63588e --- /dev/null +++ b/packages/manager/apps/sap-features-hub/public/translations/sap-features-hub/error/Messages_fr_FR.json @@ -0,0 +1,8 @@ +{ + "manager_error_page_title": "Oops …!", + "manager_error_page_button_cancel": "Annuler", + "manager_error_page_detail_code": "Code d'erreur : ", + "manager_error_page_action_reload_label": "Réessayer", + "manager_error_page_action_home_label": "Retour à la page d'accueil", + "manager_error_page_default": "Une erreur est survenue lors du chargement de la page." +} diff --git a/packages/manager/apps/sap-features-hub/src/App.tsx b/packages/manager/apps/sap-features-hub/src/App.tsx new file mode 100644 index 000000000000..da7cc51b9857 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/App.tsx @@ -0,0 +1,32 @@ +import React, { useEffect, useContext } from 'react'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { RouterProvider, createHashRouter } from 'react-router-dom'; +import { Routes } from './routes/routes'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 300_000, + }, + }, +}); + +function App() { + const { shell } = useContext(ShellContext); + const router = createHashRouter(Routes); + + useEffect(() => { + shell.ux.hidePreloader(); + }, []); + + return ( + + + + + ); +} + +export default App; diff --git a/packages/manager/apps/sap-features-hub/src/assets/error-banner-oops.png b/packages/manager/apps/sap-features-hub/src/assets/error-banner-oops.png new file mode 100644 index 000000000000..413028afad19 Binary files /dev/null and b/packages/manager/apps/sap-features-hub/src/assets/error-banner-oops.png differ diff --git a/packages/manager/apps/sap-features-hub/src/components/Breadcrumb/Breadcrumb.tsx b/packages/manager/apps/sap-features-hub/src/components/Breadcrumb/Breadcrumb.tsx new file mode 100644 index 000000000000..91551b6b8aaf --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/components/Breadcrumb/Breadcrumb.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { + OdsBreadcrumb, + OdsBreadcrumbItem, +} from '@ovhcloud/ods-components/react'; +import { + useBreadcrumb, + BreadcrumbItem, +} from '@/hooks/breadcrumb/useBreadcrumb'; +import appConfig from '@/sap-features-hub.config'; + +export interface BreadcrumbProps { + customRootLabel?: string; + appName?: string; + items?: BreadcrumbItem[]; +} + +function Breadcrumb({ customRootLabel }: BreadcrumbProps): JSX.Element { + const label = customRootLabel || appConfig.rootLabel; + + const breadcrumbItems = useBreadcrumb({ + rootLabel: label, + appName: 'sap-features-hub', + }); + return ( + + {breadcrumbItems?.map((item) => ( + + ))} + + ); +} + +export default Breadcrumb; diff --git a/packages/manager/apps/sap-features-hub/src/components/Error/Error.scss b/packages/manager/apps/sap-features-hub/src/components/Error/Error.scss new file mode 100644 index 000000000000..c73220cd3be3 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/components/Error/Error.scss @@ -0,0 +1,18 @@ +.manager-error-page { + margin-left: auto; + margin-right: auto; + max-width: 600px; + width: 100%; + display: grid; + height: 100%; + overflow: hidden; + .manager-error-page-image { + img { + width: 100%; + } + } + .manager-error-page-footer { + text-align: right; + overflow: hidden; + } +} diff --git a/packages/manager/apps/sap-features-hub/src/components/Error/Error.tsx b/packages/manager/apps/sap-features-hub/src/components/Error/Error.tsx new file mode 100644 index 000000000000..f3c146ce05c2 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/components/Error/Error.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { + ErrorMessage, + TRACKING_LABELS, + ErrorBanner, +} from '@ovh-ux/manager-react-components'; + +interface ErrorObject { + [key: string]: any; +} + +function getTrackingTypology(error: ErrorMessage) { + if (error?.detail?.status && Math.floor(error.detail.status / 100) === 4) { + return [401, 403].includes(error.detail.status) + ? TRACKING_LABELS.UNAUTHORIZED + : TRACKING_LABELS.SERVICE_NOT_FOUND; + } + return TRACKING_LABELS.PAGE_LOAD; +} + +const Errors: React.FC = ({ error }) => { + const navigate = useNavigate(); + const location = useLocation(); + const { shell } = React.useContext(ShellContext); + const { tracking, environment } = shell; + const env = environment.getEnvironment(); + + React.useEffect(() => { + env.then((response) => { + const { applicationName } = response; + const name = `errors::${getTrackingTypology(error)}::${applicationName}`; + tracking.trackPage({ + name, + level2: '81', + type: 'navigation', + page_category: location.pathname, + }); + }); + }, []); + + return ( + navigate(location.pathname, { replace: true })} + onRedirectHome={() => navigate('/', { replace: true })} + /> + ); +}; + +export default Errors; diff --git a/packages/manager/apps/sap-features-hub/src/components/Loading/Loading.tsx b/packages/manager/apps/sap-features-hub/src/components/Loading/Loading.tsx new file mode 100644 index 000000000000..4c69fd5a5a11 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/components/Loading/Loading.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { OdsSpinner } from '@ovhcloud/ods-components/react'; + +export default function Loading() { + return ( +
+
+ +
+
+ ); +} diff --git a/packages/manager/apps/sap-features-hub/src/data/api/sap-features-hub.ts b/packages/manager/apps/sap-features-hub/src/data/api/sap-features-hub.ts new file mode 100644 index 000000000000..b3f0a32acd7c --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/data/api/sap-features-hub.ts @@ -0,0 +1,52 @@ +import { fetchIcebergV6, apiClient } from '@ovh-ux/manager-core-api'; + +export type GetdedicatedCloudListParams = { + /** Filter resources on IAM tags */ + iamTags: any; +}; + +export const getdedicatedCloudListQueryKey = ['get/dedicatedCloud']; + +/** + * Operations about the PCC service : List VMware on OVHcloud infrastructures + */ +export const getdedicatedCloudList = async ( + params: GetdedicatedCloudListParams, +): Promise => apiClient.v6.get('/dedicatedCloud', { data: params }); + +export type GetdedicatedCloudServiceParams = { + /** Domain of the service */ + serviceName?: any; +}; + +export const getdedicatedCloudServiceQueryKey = ( + params: GetdedicatedCloudServiceParams, +) => [`get/dedicatedCloud/${params.serviceName}`]; + +/** + * VMware on OVHcloud : Get VMware on OVHcloud + */ +export const getdedicatedCloudService = async ( + params: GetdedicatedCloudServiceParams, +): Promise => apiClient.v6.get(`/dedicatedCloud/${params.serviceName}`); + +/** + * Get listing with iceberg V6 + */ +export const getListingIcebergV6 = async ({ + pageSize, + page, +}: { + pageSize: number; + page: number; +}) => { + const { data, status, totalCount } = await fetchIcebergV6({ + route: `/dedicatedCloud`, + pageSize, + page, + }); + if (status > 400) { + throw new Error(); + } + return { data, status, totalCount }; +}; diff --git a/packages/manager/apps/sap-features-hub/src/hooks/breadcrumb/useBreadcrumb.tsx b/packages/manager/apps/sap-features-hub/src/hooks/breadcrumb/useBreadcrumb.tsx new file mode 100644 index 000000000000..f7c1ce651801 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/hooks/breadcrumb/useBreadcrumb.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState, useContext } from 'react'; +import { useLocation } from 'react-router-dom'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; + +export type BreadcrumbItem = { + label: string | undefined; + href?: string; +}; + +export interface BreadcrumbProps { + rootLabel?: string; + appName?: string; + projectId?: string; + items?: BreadcrumbItem[]; +} +export const useBreadcrumb = ({ rootLabel, appName }: BreadcrumbProps) => { + const { shell } = useContext(ShellContext); + const [root, setRoot] = useState([]); + const [paths, setPaths] = useState([]); + const location = useLocation(); + const pathnames = location.pathname.split('/').filter((x) => x); + + useEffect(() => { + const fetchRoot = async () => { + try { + const response = await shell?.navigation.getURL(appName, '#/', {}); + const rootItem = { + label: rootLabel, + href: String(response), + }; + setRoot([rootItem]); + } catch { + // Fetch navigation error + } + }; + fetchRoot(); + }, [rootLabel, appName, shell?.navigation]); + + useEffect(() => { + const pathsTab = pathnames.map((value) => ({ + label: value, + href: `/#/${appName}/${value}`, + })); + setPaths(pathsTab); + }, [location]); + + return [...root, ...paths]; +}; diff --git a/packages/manager/apps/sap-features-hub/src/hooks/guide/useGuideUtils.tsx b/packages/manager/apps/sap-features-hub/src/hooks/guide/useGuideUtils.tsx new file mode 100644 index 000000000000..2971e624318e --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/hooks/guide/useGuideUtils.tsx @@ -0,0 +1,100 @@ +import { useContext, useEffect, useState } from 'react'; +import { CountryCode } from '@ovh-ux/manager-config'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; + +const docUrl = 'https://docs.ovh.com'; + +type GuideLinks = { [key in CountryCode]: string }; + +const GUIDE_LIST: { [guideName: string]: Partial } = { + guideLink1: { + DE: '/update-path', + ES: '/update-path', + IE: '/en/update-path', + IT: '/update-path', + PL: '/update-path', + PT: '/update-path', + FR: '/update-path', + GB: '/update-path', + CA: '/update-path', + QC: '/update-path', + WE: '/update-path', + WS: '/update-path', + US: '/update-path', + }, + guideLink2: { + DE: '/guide-link-2-path', + ES: '/guide-link-2-path', + IE: '/en/guide-link-2-path', + IT: '/guide-link-2-path', + PL: '/guide-link-2-path', + PT: '/guide-link-2-path', + FR: '/guide-link-2-path', + GB: '/guide-link-2-path', + CA: '/update-path', + QC: '/update-path', + WE: '/update-path', + WS: '/update-path', + US: '/update-path', + }, + guideLink3: { + DE: '/guide-link-3-path', + ES: '/guide-link-3-path', + IE: '/en/guide-link-3-path', + IT: '/guide-link-3-path', + PL: '/guide-link-3-path', + PT: '/guide-link-3-path', + FR: '/guide-link-3-path', + GB: '/guide-link-3-path', + CA: '/update-path', + QC: '/update-path', + WE: '/update-path', + WS: '/update-path', + US: '/update-path', + }, + /* + addNewGuideLink : { + DEFAULT: '/guide-link-3-path', + DE: '/guide-link-3-path', + ES: '/guide-link-3-path', + ... + } + */ +}; + +type GetGuideLinkProps = { + name?: string; + subsidiary: CountryCode | string; +}; + +function getGuideListLink({ subsidiary }: GetGuideLinkProps) { + const list: { [guideName: string]: string } = {}; + const keys = Object.entries(GUIDE_LIST); + keys.forEach((key) => { + list[key[0]] = docUrl + GUIDE_LIST[key[0]][subsidiary as CountryCode]; + }); + return list; +} + +interface GuideLinkProps { + [guideName: string]: string; +} + +function useGuideUtils() { + const { shell } = useContext(ShellContext); + const { environment } = shell; + const [list, setList] = useState({}); + + useEffect(() => { + const getSubSidiary = async () => { + const env = await environment.getEnvironment(); + const { ovhSubsidiary } = env.getUser(); + const guideList = getGuideListLink({ subsidiary: ovhSubsidiary }); + setList(guideList); + }; + getSubSidiary(); + }, []); + return list as GuideLinkProps; +} + +export default useGuideUtils; diff --git a/packages/manager/apps/sap-features-hub/src/index.scss b/packages/manager/apps/sap-features-hub/src/index.scss new file mode 100644 index 000000000000..bb4185167bd1 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/index.scss @@ -0,0 +1,7 @@ +@tailwind utilities; + +@import '@ovhcloud/ods-themes/default'; + +html { + font-family: var(--ods-font-family-default); +} diff --git a/packages/manager/apps/sap-features-hub/src/index.tsx b/packages/manager/apps/sap-features-hub/src/index.tsx new file mode 100644 index 000000000000..a2facf9a2a72 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/index.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { + ShellContext, + initShellContext, + initI18n, +} from '@ovh-ux/manager-react-shell-client'; +import '@ovh-ux/manager-react-components/dist/style.css'; +import App from './App'; +import './vite-hmr'; +import './index.scss'; + +import { UNIVERSE, SUB_UNIVERSE, APP_NAME, LEVEL2 } from './tracking.constant'; + +const trackingContext = { + chapter1: UNIVERSE, + chapter2: SUB_UNIVERSE, + chapter3: APP_NAME, + appName: APP_NAME, + pageTheme: UNIVERSE, + level2Config: LEVEL2, +}; + +const init = async (appName: string) => { + const context = await initShellContext(appName, trackingContext); + + await initI18n({ + context, + reloadOnLocaleChange: true, + defaultNS: appName, + ns: ['listing', 'dashboard', 'onboarding'], + }); + + const region = context.environment.getRegion(); + context.shell.tracking.setConfig(region, LEVEL2); + try { + await import(`./config-${region}.js`); + } catch (error) { + // nothing to do + } + + ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , + ); +}; + +init('sap-features-hub'); diff --git a/packages/manager/apps/sap-features-hub/src/pages/404.tsx b/packages/manager/apps/sap-features-hub/src/pages/404.tsx new file mode 100644 index 000000000000..d052f1ebcbbf --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/404.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export default function NotFound() { + // @TODO: add a redirection here in order to catch /:serviceName given from iframe + + return

404 - route not found

; +} diff --git a/packages/manager/apps/sap-features-hub/src/pages/dashboard/general-informations/index.tsx b/packages/manager/apps/sap-features-hub/src/pages/dashboard/general-informations/index.tsx new file mode 100644 index 000000000000..a785e6d2ff0f --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/dashboard/general-informations/index.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function GeneralInfos() { + return
Information Générales Tab
; +} + +export default GeneralInfos; diff --git a/packages/manager/apps/sap-features-hub/src/pages/dashboard/index.tsx b/packages/manager/apps/sap-features-hub/src/pages/dashboard/index.tsx new file mode 100644 index 000000000000..8ccc127990db --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/dashboard/index.tsx @@ -0,0 +1,81 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Outlet, + NavLink, + useLocation, + useNavigate, + useParams, + useResolvedPath, +} from 'react-router-dom'; +import { OdsTabs, OdsTab } from '@ovhcloud/ods-components/react'; + +import { BaseLayout } from '@ovh-ux/manager-react-components'; + +import Breadcrumb from '@/components/Breadcrumb/Breadcrumb'; + +export type DashboardTabItemProps = { + name: string; + title: string; + to: string; +}; + +export type DashboardLayoutProps = { + tabs: DashboardTabItemProps[]; +}; + +export default function DashboardPage() { + const [panel, setActivePanel] = useState(''); + const { serviceName } = useParams(); + const location = useLocation(); + const navigate = useNavigate(); + const { t } = useTranslation('dashboard'); + + const tabsList = [ + { + name: 'general_informations', + title: 'Informations générales', + to: useResolvedPath('').pathname, + }, + { + name: 'Tab 2', + title: 'Tab 2', + to: useResolvedPath('Tab2').pathname, + }, + ]; + + useEffect(() => { + const activeTab = tabsList.find((tab) => tab.to === location.pathname); + if (activeTab) { + setActivePanel(activeTab.name); + } else { + setActivePanel(tabsList[0].name); + navigate(`${tabsList[0].to}`); + } + }, [location.pathname]); + + const header = { + title: t('title'), + }; + + return ( + } + header={header} + description="Description du sap-features-hub" + tabs={ + + {tabsList.map((tab: DashboardTabItemProps) => ( + + + {tab.title} + + + ))} + + } + > + + + ); +} diff --git a/packages/manager/apps/sap-features-hub/src/pages/dashboard/tab2/index.tsx b/packages/manager/apps/sap-features-hub/src/pages/dashboard/tab2/index.tsx new file mode 100644 index 000000000000..9cb100623734 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/dashboard/tab2/index.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function Tab2() { + return
Tab 2
; +} + +export default Tab2; diff --git a/packages/manager/apps/sap-features-hub/src/pages/index.tsx b/packages/manager/apps/sap-features-hub/src/pages/index.tsx new file mode 100644 index 000000000000..84f751a77920 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export default function SapFeaturesHub() { + const { t } = useTranslation('sap-features-hub'); + + return ( +
+

{t('title')}

+
Start your application
+
+ ); +} diff --git a/packages/manager/apps/sap-features-hub/src/pages/layout.tsx b/packages/manager/apps/sap-features-hub/src/pages/layout.tsx new file mode 100644 index 000000000000..47fdd4b624c9 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/layout.tsx @@ -0,0 +1,31 @@ +import React, { useEffect, useContext } from 'react'; +import { defineCurrentPage } from '@ovh-ux/request-tagger'; +import { Outlet, useLocation, useMatches } from 'react-router-dom'; +import { + useOvhTracking, + useRouteSynchro, + ShellContext, +} from '@ovh-ux/manager-react-shell-client'; + +export default function Layout() { + const location = useLocation(); + const { shell } = useContext(ShellContext); + const matches = useMatches(); + const { trackCurrentPage } = useOvhTracking(); + useRouteSynchro(); + + useEffect(() => { + const match = matches.slice(-1); + defineCurrentPage(`app.sap-features-hub-${match[0]?.id}`); + }, [location]); + + useEffect(() => { + trackCurrentPage(); + }, [location]); + + useEffect(() => { + shell.ux.hidePreloader(); + }, []); + + return ; +} diff --git a/packages/manager/apps/sap-features-hub/src/pages/listing/index.tsx b/packages/manager/apps/sap-features-hub/src/pages/listing/index.tsx new file mode 100644 index 000000000000..c09c85d25991 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/listing/index.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useLocation } from 'react-router-dom'; + +import { OdsButton } from '@ovhcloud/ods-components/react'; +import { ODS_BUTTON_VARIANT } from '@ovhcloud/ods-components'; + +import { + Datagrid, + DataGridTextCell, + useResourcesIcebergV6, + dataType, + BaseLayout, +} from '@ovh-ux/manager-react-components'; + +import Loading from '@/components/Loading/Loading'; +import ErrorBanner from '@/components/Error/Error'; +import Breadcrumb from '@/components/Breadcrumb/Breadcrumb'; + +import appConfig from '@/sap-features-hub.config'; + +export default function Listing() { + const myConfig = appConfig; + const serviceKey = myConfig.listing?.datagrid?.serviceKey; + const [columns, setColumns] = useState([]); + const navigate = useNavigate(); + const location = useLocation(); + const { t } = useTranslation('listing'); + const { + flattenData, + isError, + error, + totalCount, + hasNextPage, + fetchNextPage, + isLoading, + status, + sorting, + setSorting, + pageIndex, + } = useResourcesIcebergV6({ + route: `/dedicatedCloud`, + queryKey: ['sap-features-hub', `/dedicatedCloud`], + }); + + const navigateToDashboard = (label: string) => { + const path = + location.pathname.indexOf('pci') > -1 ? `${location.pathname}/` : '/'; + navigate(`${path}${label}`); + }; + + useEffect(() => { + if (columns && status === 'success' && flattenData?.length > 0) { + const newColumns = Object.keys(flattenData[0]) + .filter((element) => element !== 'iam') + .map((element) => ({ + id: element, + label: element, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + type: dataType(flattenData[0][element]), + cell: (props: any) => { + const label = props[element] as string; + if (typeof label === 'string' || typeof label === 'number') { + if (serviceKey === element) + return ( + + navigateToDashboard(label)} + label={label} + /> + + ); + return {label}; + } + return
-
; + }, + })); + setColumns(newColumns); + } + }, [flattenData]); + + if (isError) { + return ; + } + + if (isLoading && pageIndex === 1) { + return ( +
+ +
+ ); + } + + const header = { + title: t('title'), + }; + + return ( + } header={header}> + + {columns && flattenData && ( + + )} + + + ); +} diff --git a/packages/manager/apps/sap-features-hub/src/pages/listing/listing.spec.tsx b/packages/manager/apps/sap-features-hub/src/pages/listing/listing.spec.tsx new file mode 100644 index 000000000000..722f04de9e63 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/listing/listing.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import { describe, it, vi, expect } from 'vitest'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import Listing from './index'; + +vi.mock('@ovh-ux/manager-react-components', () => ({ + BaseLayout: vi.fn().mockReturnValue(
), + useResourcesIcebergV6: vi.fn().mockReturnValue({ + flattenData: [], + isError: false, + error: {}, + totalCount: 0, + hasNextPage: true, + fetchNextPage: vi.fn(), + isLoading: true, + status: {}, + sorting: [], + setSorting: false, + pageIndex: 1, + }), + Datagrid: vi.fn().mockReturnValue(
), +})); + +vi.mock('react-router-dom', () => ({ + useNavigate: vi.fn(), + useLocation: vi.fn(() => ({ + hash: '', + key: 'default', + pathname: '/', + search: '', + state: null, + })), +})); + +const queryClient = new QueryClient(); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const wrapper = ({ children }) => ( + {children} +); + +describe('listing page', () => { + it('displays loading spinner while main request are loading', () => { + const { getByTestId } = render(, { wrapper }); + expect(getByTestId('listing-page-spinner')).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/sap-features-hub/src/pages/onboarding/index.scss b/packages/manager/apps/sap-features-hub/src/pages/onboarding/index.scss new file mode 100644 index 000000000000..995e5c3b0be6 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/onboarding/index.scss @@ -0,0 +1,10 @@ +.tile-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-gap: 30px; + padding-top: 3rem; + + @media (min-width: 1024px) { + grid-template-columns: repeat(3, 1fr); + } +} diff --git a/packages/manager/apps/sap-features-hub/src/pages/onboarding/index.tsx b/packages/manager/apps/sap-features-hub/src/pages/onboarding/index.tsx new file mode 100644 index 000000000000..93e59c16a128 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/pages/onboarding/index.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Card, OnboardingLayout } from '@ovh-ux/manager-react-components'; +import useGuideUtils from '@/hooks/guide/useGuideUtils'; +import Breadcrumb from '@/components/Breadcrumb/Breadcrumb'; +import onboardingImgSrc from './onboarding-img.png'; + +export default function Onboarding() { + const { t } = useTranslation('onboarding'); + const link = useGuideUtils(); + + const tileList = [ + { + id: 1, + texts: { + title: t('guide1Title'), + description: t('guide1Description'), + category: t('guideCategory'), + }, + href: link?.guideLink1, + }, + { + id: 2, + texts: { + title: t('guide2Title'), + description: t('guide2Description'), + category: t('guideCategory'), + }, + href: link?.guideLink2, + }, + { + id: 3, + texts: { + title: t('guide3Title'), + description: t('guide3Description'), + category: t('guideCategory'), + }, + href: link?.guideLink3, + }, + ]; + + const title: string = t('title'); + const description: string = t('description'); + const imgSrc = { + src: onboardingImgSrc, + }; + + return ( + <> + + + {tileList.map((tile) => ( + + ))} + + + ); +} diff --git a/packages/manager/apps/sap-features-hub/src/pages/onboarding/onboarding-img.png b/packages/manager/apps/sap-features-hub/src/pages/onboarding/onboarding-img.png new file mode 100644 index 000000000000..1ac8d6473c95 Binary files /dev/null and b/packages/manager/apps/sap-features-hub/src/pages/onboarding/onboarding-img.png differ diff --git a/packages/manager/apps/sap-features-hub/src/routes/routes.constant.ts b/packages/manager/apps/sap-features-hub/src/routes/routes.constant.ts new file mode 100644 index 000000000000..8fcb96e6009d --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/routes/routes.constant.ts @@ -0,0 +1,7 @@ +export const urls = { + root: '/', + onboarding: '/onboarding', + listing: '/', + dashboard: '/:serviceName', + tab2: 'Tab2', +}; diff --git a/packages/manager/apps/sap-features-hub/src/routes/routes.tsx b/packages/manager/apps/sap-features-hub/src/routes/routes.tsx new file mode 100644 index 000000000000..7cdcd5e49041 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/routes/routes.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { RouteObject } from 'react-router-dom'; +import { PageType } from '@ovh-ux/manager-react-shell-client'; +import NotFound from '@/pages/404'; +import { urls } from '@/routes/routes.constant'; + +const lazyRouteConfig = (importFn: CallableFunction): Partial => { + return { + lazy: async () => { + const { default: moduleDefault, ...moduleExports } = await importFn(); + return { + Component: moduleDefault, + ...moduleExports, + }; + }, + }; +}; + +export const Routes: any = [ + { + path: urls.root, + ...lazyRouteConfig(() => import('@/pages/layout')), + children: [ + { + id: 'listing', + path: urls.listing, + ...lazyRouteConfig(() => import('@/pages/listing')), + handle: { + tracking: { + pageName: 'listing', + pageType: PageType.listing, + }, + }, + }, + { + path: urls.dashboard, + ...lazyRouteConfig(() => import('@/pages/dashboard')), + children: [ + { + id: 'dashboard', + path: '', + ...lazyRouteConfig(() => + import('@/pages/dashboard/general-informations'), + ), + handle: { + tracking: { + pageName: 'dashboard', + pageType: PageType.dashboard, + }, + }, + }, + { + id: 'dashboard.tab2', + path: 'Tab2', + ...lazyRouteConfig(() => import('@/pages/dashboard/tab2')), + handle: { + tracking: { + pageName: 'tab2', + pageType: PageType.dashboard, + }, + }, + }, + ], + }, + { + id: 'onboarding', + path: urls.onboarding, + ...lazyRouteConfig(() => import('@/pages/onboarding')), + handle: { + tracking: { + pageName: 'onboarding', + pageType: PageType.onboarding, + }, + }, + }, + ], + }, + { + path: '*', + element: , + }, +]; diff --git a/packages/manager/apps/sap-features-hub/src/sap-features-hub.config.ts b/packages/manager/apps/sap-features-hub/src/sap-features-hub.config.ts new file mode 100644 index 000000000000..8fa43a08a249 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/sap-features-hub.config.ts @@ -0,0 +1,8 @@ +export default { + listing: { + datagrid: { + serviceKey: 'name', + }, + }, + rootLabel: 'sap-features-hub', +}; diff --git a/packages/manager/apps/sap-features-hub/src/tracking.constant.ts b/packages/manager/apps/sap-features-hub/src/tracking.constant.ts new file mode 100644 index 000000000000..b02f40ec8e75 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/tracking.constant.ts @@ -0,0 +1,20 @@ +export const LEVEL2 = { + EU: { + config: { + level2: '0', + }, + }, + CA: { + config: { + level2: '0', + }, + }, + US: { + config: { + level2: '0', + }, + }, +}; +export const UNIVERSE = 'HostedPrivatedCloud'; +export const SUB_UNIVERSE = 'HostedPrivatedCloud'; +export const APP_NAME = 'sap-features-hub'; diff --git a/packages/manager/apps/sap-features-hub/src/vite-hmr.ts b/packages/manager/apps/sap-features-hub/src/vite-hmr.ts new file mode 100644 index 000000000000..473d87630039 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/src/vite-hmr.ts @@ -0,0 +1,5 @@ +if (import.meta.hot) { + import.meta.hot.on('iframe-reload', () => { + window.location.reload(); + }); +} diff --git a/packages/manager/apps/sap-features-hub/tailwind.config.js b/packages/manager/apps/sap-features-hub/tailwind.config.js new file mode 100644 index 000000000000..9697fd991205 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/tailwind.config.js @@ -0,0 +1,22 @@ +import path from 'path'; + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './src/**/*.{js,jsx,ts,tsx}', + path.join( + path.dirname(require.resolve('@ovh-ux/manager-react-components')), + '**/*.{js,jsx,ts,tsx}', + ), + ], + theme: { + screens: { + xs: '0', + sm: '36em', + md: '48em', + lg: '62em', + xl: '75em', + xxl: '87.5em', + }, + }, +}; diff --git a/packages/manager/apps/sap-features-hub/tsconfig.json b/packages/manager/apps/sap-features-hub/tsconfig.json new file mode 100644 index 000000000000..e2104f471575 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["dom", "es2020"], + "noEmit": true, + "target": "es2020", + "types": ["vite/client", "node"], + "module": "ES2020", + "moduleResolution": "node", + "removeComments": true, + "outDir": "dist", + "esModuleInterop": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "noImplicitAny": true, + "declaration": true, + "resolveJsonModule": true, + "allowJs": true, + "jsx": "react", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "types", "src/__tests__"] +} diff --git a/packages/manager/apps/sap-features-hub/vite.config.mjs b/packages/manager/apps/sap-features-hub/vite.config.mjs new file mode 100644 index 000000000000..f33ab6dc98cd --- /dev/null +++ b/packages/manager/apps/sap-features-hub/vite.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite'; +import { getBaseConfig } from '@ovh-ux/manager-vite-config'; +import { resolve } from 'path'; + +export default defineConfig({ + ...getBaseConfig(), + root: resolve(process.cwd()), +}); diff --git a/packages/manager/apps/sap-features-hub/vitest.config.js b/packages/manager/apps/sap-features-hub/vitest.config.js new file mode 100644 index 000000000000..8da64fe8a448 --- /dev/null +++ b/packages/manager/apps/sap-features-hub/vitest.config.js @@ -0,0 +1,34 @@ +import path from 'path'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: [], + coverage: { + include: ['src'], + exclude: [], + }, + testTimeout: 60_000, + fileParallelism: false, + maxWorkers: 1, + pollOptions: { + forks: { + singleFork: true, + }, + threads: { + singleThread: true, + }, + }, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + mainFields: ['module'], + }, +});