diff --git a/packages/manager/apps/ips/README.md b/packages/manager/apps/ips/README.md
new file mode 100644
index 000000000000..616579cd8100
--- /dev/null
+++ b/packages/manager/apps/ips/README.md
@@ -0,0 +1,3 @@
+# @ovh-ux/manager-ips-app
+
+> ips manager app
diff --git a/packages/manager/apps/ips/cucumber.js b/packages/manager/apps/ips/cucumber.js
new file mode 100644
index 000000000000..8e6abbfbca8f
--- /dev/null
+++ b/packages/manager/apps/ips/cucumber.js
@@ -0,0 +1,20 @@
+const isCI = process.env.CI;
+
+module.exports = {
+ default: {
+ paths: ['e2e/features/**/*.feature'],
+ require: [
+ '../../../../playwright-helpers/bdd-setup.ts',
+ 'e2e/**/*.step.ts',
+ ],
+ requireModule: ['ts-node/register'],
+ format: [
+ 'summary',
+ isCI ? 'progress' : 'progress-bar',
+ !isCI && ['html', 'e2e/reports/cucumber-results-report.html'],
+ !isCI && ['usage-json', 'e2e/reports/cucumber-usage-report.json'],
+ ].filter(Boolean),
+ formatOptions: { snippetInterface: 'async-await' },
+ retry: 1,
+ },
+};
diff --git a/packages/manager/apps/ips/index.html b/packages/manager/apps/ips/index.html
new file mode 100644
index 000000000000..ec4e92d05007
--- /dev/null
+++ b/packages/manager/apps/ips/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+ OVHcloud
+
+
+
+
+
+
+
+
+
diff --git a/packages/manager/apps/ips/mocks/example/example-data.json b/packages/manager/apps/ips/mocks/example/example-data.json
new file mode 100644
index 000000000000..16b09c4e47fd
--- /dev/null
+++ b/packages/manager/apps/ips/mocks/example/example-data.json
@@ -0,0 +1,11 @@
+[
+ {
+ "id": 20374
+ },
+ {
+ "id": 20375
+ },
+ {
+ "id": 20379
+ }
+]
diff --git a/packages/manager/apps/ips/mocks/example/example.ts b/packages/manager/apps/ips/mocks/example/example.ts
new file mode 100644
index 000000000000..77c033e7b392
--- /dev/null
+++ b/packages/manager/apps/ips/mocks/example/example.ts
@@ -0,0 +1,30 @@
+import { Handler } from '../../../../../../playwright-helpers';
+import exampleList from './example-data.json';
+
+export type GetExampleMocksParams = { isKo?: boolean; nb?: number };
+
+export const getExampleMocks = ({
+ isKo,
+ nb = Number.POSITIVE_INFINITY,
+}: GetExampleMocksParams): Handler[] => [
+ {
+ url: '*',
+ response: isKo
+ ? {
+ message: 'Example error',
+ }
+ : exampleList.slice(0, nb),
+ status: isKo ? 500 : 200,
+ api: 'v6',
+ },
+ {
+ url: '*',
+ response: isKo
+ ? {
+ message: 'Example error',
+ }
+ : exampleList.slice(0, nb),
+ status: isKo ? 500 : 200,
+ api: 'v2',
+ },
+];
diff --git a/packages/manager/apps/ips/mocks/index.ts b/packages/manager/apps/ips/mocks/index.ts
new file mode 100644
index 000000000000..4356d0ac05ac
--- /dev/null
+++ b/packages/manager/apps/ips/mocks/index.ts
@@ -0,0 +1 @@
+export * from './example/example';
diff --git a/packages/manager/apps/ips/package.json b/packages/manager/apps/ips/package.json
new file mode 100644
index 000000000000..d1633b1ec3eb
--- /dev/null
+++ b/packages/manager/apps/ips/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@ovh-ux/manager-ips-app",
+ "version": "0.0.0",
+ "private": true,
+ "description": "Ips manager app",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ovh/manager.git",
+ "directory": "packages/manager/apps/ips"
+ },
+ "license": "BSD-3-Clause",
+ "author": "OVH SAS",
+ "scripts": {
+ "build": "tsc && vite build",
+ "dev": "tsc && vite",
+ "start": "lerna exec --stream --scope='@ovh-ux/manager-ips-app' --include-dependencies -- npm run build --if-present",
+ "start:dev": "lerna exec --stream --scope='@ovh-ux/manager-ips-app' --include-dependencies -- npm run dev --if-present",
+ "start:watch": "lerna exec --stream --parallel --scope='@ovh-ux/manager-ips-app' --include-dependencies -- npm run dev:watch --if-present"
+ },
+ "dependencies": {
+ "@ovh-ux/manager-config": "*",
+ "@ovh-ux/manager-core-api": "*",
+ "@ovh-ux/manager-core-utils": "*",
+ "@ovh-ux/manager-react-components": "2.1.0",
+ "@ovh-ux/manager-react-core-application": "*",
+ "@ovh-ux/manager-react-shell-client": "^0.8.1",
+ "@ovh-ux/manager-tailwind-config": "*",
+ "@ovh-ux/request-tagger": "^0.4.0",
+ "@ovhcloud/ods-common-core": "^18.3.0",
+ "@ovhcloud/ods-components": "18.3.0",
+ "@ovhcloud/ods-themes": "^18.3.0",
+ "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"
+ },
+ "devDependencies": {
+ "@cucumber/cucumber": "^10.3.1",
+ "@ovh-ux/manager-vite-config": "*",
+ "@tanstack/react-query": "^5.51.21",
+ "@tanstack/react-query-devtools": "^5.51.21",
+ "tailwindcss": "^3.4.4",
+ "typescript": "^5.1.6",
+ "vite": "^5.2.13"
+ },
+ "regions": [
+ "CA",
+ "EU",
+ "US"
+ ]
+}
diff --git a/packages/manager/apps/ips/postcss.config.js b/packages/manager/apps/ips/postcss.config.js
new file mode 100644
index 000000000000..12a703d900da
--- /dev/null
+++ b/packages/manager/apps/ips/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/packages/manager/apps/ips/public/translations/dashboard/Messages_fr_FR.json b/packages/manager/apps/ips/public/translations/dashboard/Messages_fr_FR.json
new file mode 100644
index 000000000000..f42a27b3366a
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/public/translations/ips/Messages_fr_FR.json b/packages/manager/apps/ips/public/translations/ips/Messages_fr_FR.json
new file mode 100644
index 000000000000..c5bf7278da50
--- /dev/null
+++ b/packages/manager/apps/ips/public/translations/ips/Messages_fr_FR.json
@@ -0,0 +1,6 @@
+{
+ "title": "Bienvenue uapp",
+ "crumb": "ips",
+ "tabs_2": "Tabs 2",
+ "onboarding": "Onboarding"
+}
diff --git a/packages/manager/apps/ips/public/translations/ips/error/Messages_fr_FR.json b/packages/manager/apps/ips/public/translations/ips/error/Messages_fr_FR.json
new file mode 100644
index 000000000000..2c575c63588e
--- /dev/null
+++ b/packages/manager/apps/ips/public/translations/ips/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/ips/public/translations/listing/Messages_fr_FR.json b/packages/manager/apps/ips/public/translations/listing/Messages_fr_FR.json
new file mode 100644
index 000000000000..c882bc92cfec
--- /dev/null
+++ b/packages/manager/apps/ips/public/translations/listing/Messages_fr_FR.json
@@ -0,0 +1,4 @@
+{
+ "title": "Listing page",
+ "listing_resultats": "résultats"
+}
diff --git a/packages/manager/apps/ips/public/translations/onboarding/Messages_fr_FR.json b/packages/manager/apps/ips/public/translations/onboarding/Messages_fr_FR.json
new file mode 100644
index 000000000000..2866b83cbef9
--- /dev/null
+++ b/packages/manager/apps/ips/public/translations/onboarding/Messages_fr_FR.json
@@ -0,0 +1,13 @@
+{
+ "title": "ips",
+ "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 ips",
+ "moreInfoButtonLabel": "En savoir plus sur ips",
+ "guideCategory": "Tutoriel",
+ "guide1Title": "Premiers pas avec un ips",
+ "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/ips/src/App.tsx b/packages/manager/apps/ips/src/App.tsx
new file mode 100644
index 000000000000..9bedb39c2cae
--- /dev/null
+++ b/packages/manager/apps/ips/src/App.tsx
@@ -0,0 +1,35 @@
+import React, { useEffect, useContext } from 'react';
+import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import { odsSetup } from '@ovhcloud/ods-common-core';
+import { ShellContext } from '@ovh-ux/manager-react-shell-client';
+import { RouterProvider, createHashRouter } from 'react-router-dom';
+import { Routes } from './routes/routes';
+
+odsSetup();
+
+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/ips/src/assets/error-banner-oops.png b/packages/manager/apps/ips/src/assets/error-banner-oops.png
new file mode 100644
index 000000000000..413028afad19
Binary files /dev/null and b/packages/manager/apps/ips/src/assets/error-banner-oops.png differ
diff --git a/packages/manager/apps/ips/src/components/Breadcrumb/Breadcrumb.tsx b/packages/manager/apps/ips/src/components/Breadcrumb/Breadcrumb.tsx
new file mode 100644
index 000000000000..aebed8e4fee7
--- /dev/null
+++ b/packages/manager/apps/ips/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 '@/ips.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: 'ips',
+ });
+ return (
+
+ {breadcrumbItems?.map((item) => (
+
+ ))}
+
+ );
+}
+
+export default Breadcrumb;
diff --git a/packages/manager/apps/ips/src/components/Error/Error.scss b/packages/manager/apps/ips/src/components/Error/Error.scss
new file mode 100644
index 000000000000..c73220cd3be3
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/src/components/Error/Error.tsx b/packages/manager/apps/ips/src/components/Error/Error.tsx
new file mode 100644
index 000000000000..f3c146ce05c2
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/src/components/Loading/Loading.tsx b/packages/manager/apps/ips/src/components/Loading/Loading.tsx
new file mode 100644
index 000000000000..4c69fd5a5a11
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/src/data/api/ips.ts b/packages/manager/apps/ips/src/data/api/ips.ts
new file mode 100644
index 000000000000..a743317ae15f
--- /dev/null
+++ b/packages/manager/apps/ips/src/data/api/ips.ts
@@ -0,0 +1,64 @@
+import { fetchIcebergV6, apiClient } from '@ovh-ux/manager-core-api';
+
+export type GetIpListParams = {
+ /** Filter the value of campus property (ilike) (alpha) */
+ campus: any;
+ /** Filter the value of description property (like) */
+ description: any;
+ /** Filter resources on IAM tags */
+ iamTags: any;
+ /** Filter the value of ip property (contains or equals) */
+ ip: any;
+ /** Filter the value of isAdditionalIp property (=) (alpha) */
+ isAdditionalIp: any;
+ /** Filter the value of routedTo.serviceName property (like) */
+ routedToserviceName: any;
+ /** Filter the value of type property (=) */
+ type: any;
+ /** Filter the value of version property (=) (alpha) */
+ version: any;
+};
+
+export const getIpListQueryKey = ['get/ip'];
+
+/**
+ * List the ip.Ip objects : Your OVH IPs
+ */
+export const getIpList = async (params: GetIpListParams): Promise =>
+ apiClient.v6.get('/ip', { data: params });
+
+export type GetIpIpParams = {
+ /** */
+ ip?: any;
+};
+
+export const getIpIpQueryKey = (params: GetIpIpParams) => [
+ `get/ip/${params.ip}`,
+];
+
+/**
+ * Your IP : Get this object properties
+ */
+export const getIpIp = async (params: GetIpIpParams): Promise =>
+ apiClient.v6.get(`/ip/${params.ip}`);
+
+/**
+ * Get listing with iceberg V6
+ */
+export const getListingIcebergV6 = async ({
+ pageSize,
+ page,
+}: {
+ pageSize: number;
+ page: number;
+}) => {
+ const { data, status, totalCount } = await fetchIcebergV6({
+ route: `/ip`,
+ pageSize,
+ page,
+ });
+ if (status > 400) {
+ throw new Error();
+ }
+ return { data, status, totalCount };
+};
diff --git a/packages/manager/apps/ips/src/hooks/breadcrumb/useBreadcrumb.tsx b/packages/manager/apps/ips/src/hooks/breadcrumb/useBreadcrumb.tsx
new file mode 100644
index 000000000000..d61852b056a4
--- /dev/null
+++ b/packages/manager/apps/ips/src/hooks/breadcrumb/useBreadcrumb.tsx
@@ -0,0 +1,49 @@
+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/ips/src/hooks/guide/useGuideUtils.tsx b/packages/manager/apps/ips/src/hooks/guide/useGuideUtils.tsx
new file mode 100644
index 000000000000..2971e624318e
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/src/index.scss b/packages/manager/apps/ips/src/index.scss
new file mode 100644
index 000000000000..bb4185167bd1
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/src/index.tsx b/packages/manager/apps/ips/src/index.tsx
new file mode 100644
index 000000000000..962033765923
--- /dev/null
+++ b/packages/manager/apps/ips/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 App from './App';
+import '@ovhcloud/ods-themes/default';
+import './index.scss';
+import './vite-hmr';
+
+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('ips');
diff --git a/packages/manager/apps/ips/src/ips.config.ts b/packages/manager/apps/ips/src/ips.config.ts
new file mode 100644
index 000000000000..1bdac937e1dd
--- /dev/null
+++ b/packages/manager/apps/ips/src/ips.config.ts
@@ -0,0 +1,8 @@
+export default {
+ listing: {
+ datagrid: {
+ serviceKey: 'ip',
+ },
+ },
+ rootLabel: 'ips',
+};
diff --git a/packages/manager/apps/ips/src/pages/404.tsx b/packages/manager/apps/ips/src/pages/404.tsx
new file mode 100644
index 000000000000..d052f1ebcbbf
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/src/pages/index.tsx b/packages/manager/apps/ips/src/pages/index.tsx
new file mode 100644
index 000000000000..2b1fec26d6fe
--- /dev/null
+++ b/packages/manager/apps/ips/src/pages/index.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+export default function Ips() {
+ const { t } = useTranslation('ips');
+
+ return (
+
+
{t('title')}
+
Start your application
+
+ );
+}
diff --git a/packages/manager/apps/ips/src/pages/layout.tsx b/packages/manager/apps/ips/src/pages/layout.tsx
new file mode 100644
index 000000000000..81ae31937de8
--- /dev/null
+++ b/packages/manager/apps/ips/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.ips-${match[0]?.id}`);
+ }, [location]);
+
+ useEffect(() => {
+ trackCurrentPage();
+ }, [location]);
+
+ useEffect(() => {
+ shell.ux.hidePreloader();
+ }, []);
+
+ return ;
+}
diff --git a/packages/manager/apps/ips/src/pages/listing/index.tsx b/packages/manager/apps/ips/src/pages/listing/index.tsx
new file mode 100644
index 000000000000..9f1fee6f8963
--- /dev/null
+++ b/packages/manager/apps/ips/src/pages/listing/index.tsx
@@ -0,0 +1,101 @@
+import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate, useLocation, Navigate } from 'react-router-dom';
+
+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 '@/ips.config';
+import { urls } from '@/routes/routes.constant';
+
+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: `/ip`,
+ queryKey: ['ips', `/ip`],
+ });
+
+ if (!isLoading && flattenData?.length === 0) {
+ navigate(urls.onboarding);
+ }
+
+ useEffect(() => {
+ if (columns && status === 'success' && flattenData?.length > 0) {
+ const newColumns = Object.keys(flattenData[0])
+ .filter((element) => element !== 'iam')
+ .map((element) => ({
+ id: element,
+ label: element,
+ type: 'string',
+ cell: (props: any) => {
+ const label = props[element] as string;
+ if (typeof label === 'string' || typeof label === 'number') {
+ 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/ips/src/pages/onboarding/index.scss b/packages/manager/apps/ips/src/pages/onboarding/index.scss
new file mode 100644
index 000000000000..995e5c3b0be6
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/src/pages/onboarding/index.tsx b/packages/manager/apps/ips/src/pages/onboarding/index.tsx
new file mode 100644
index 000000000000..93e59c16a128
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/src/pages/onboarding/onboarding-img.png b/packages/manager/apps/ips/src/pages/onboarding/onboarding-img.png
new file mode 100644
index 000000000000..1ac8d6473c95
Binary files /dev/null and b/packages/manager/apps/ips/src/pages/onboarding/onboarding-img.png differ
diff --git a/packages/manager/apps/ips/src/routes/routes.constant.ts b/packages/manager/apps/ips/src/routes/routes.constant.ts
new file mode 100644
index 000000000000..a685b2c8fbb4
--- /dev/null
+++ b/packages/manager/apps/ips/src/routes/routes.constant.ts
@@ -0,0 +1,11 @@
+export const subRoutes = {
+ root: '/ip',
+ onboarding: 'onboarding',
+ order: 'order',
+};
+
+export const urls = {
+ root: subRoutes.root,
+ onboarding: `${subRoutes.root}/${subRoutes.onboarding}`,
+ listing: `${subRoutes.root}`,
+};
diff --git a/packages/manager/apps/ips/src/routes/routes.tsx b/packages/manager/apps/ips/src/routes/routes.tsx
new file mode 100644
index 000000000000..3c538fa5cfd2
--- /dev/null
+++ b/packages/manager/apps/ips/src/routes/routes.tsx
@@ -0,0 +1,52 @@
+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: '/',
+ ...lazyRouteConfig(() => import('@/pages/layout')),
+ children: [
+ {
+ id: 'listing',
+ path: urls.listing,
+ ...lazyRouteConfig(() => import('@/pages/listing')),
+ handle: {
+ tracking: {
+ pageName: 'listing',
+ pageType: PageType.listing,
+ },
+ },
+ },
+ {
+ id: 'onboarding',
+ path: urls.onboarding,
+ ...lazyRouteConfig(() => import('@/pages/onboarding')),
+ handle: {
+ tracking: {
+ pageName: 'onboarding',
+ pageType: PageType.onboarding,
+ },
+ },
+ },
+ ],
+ },
+ {
+ path: '*',
+ element: ,
+ },
+];
diff --git a/packages/manager/apps/ips/src/tracking.constant.ts b/packages/manager/apps/ips/src/tracking.constant.ts
new file mode 100644
index 000000000000..5b17de838dc1
--- /dev/null
+++ b/packages/manager/apps/ips/src/tracking.constant.ts
@@ -0,0 +1,20 @@
+export const LEVEL2 = {
+ EU: {
+ config: {
+ level2: '85',
+ },
+ },
+ CA: {
+ config: {
+ level2: '85',
+ },
+ },
+ US: {
+ config: {
+ level2: '57',
+ },
+ },
+};
+export const UNIVERSE = 'Dedicated';
+export const SUB_UNIVERSE = 'Network';
+export const APP_NAME = 'ips';
diff --git a/packages/manager/apps/ips/src/vite-hmr.ts b/packages/manager/apps/ips/src/vite-hmr.ts
new file mode 100644
index 000000000000..473d87630039
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/tailwind.config.js b/packages/manager/apps/ips/tailwind.config.js
new file mode 100644
index 000000000000..657ab11bb87d
--- /dev/null
+++ b/packages/manager/apps/ips/tailwind.config.js
@@ -0,0 +1,14 @@
+import path from 'path';
+import config from '@ovh-ux/manager-tailwind-config';
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ ...config,
+ content: [
+ './src/**/*.{js,jsx,ts,tsx}',
+ path.join(
+ path.dirname(require.resolve('@ovh-ux/manager-react-components')),
+ '**/*.{js,jsx,ts,tsx}',
+ ),
+ ],
+};
diff --git a/packages/manager/apps/ips/tsconfig.json b/packages/manager/apps/ips/tsconfig.json
new file mode 100644
index 000000000000..e2104f471575
--- /dev/null
+++ b/packages/manager/apps/ips/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/ips/tsconfig.test.json b/packages/manager/apps/ips/tsconfig.test.json
new file mode 100644
index 000000000000..7048c297c8f6
--- /dev/null
+++ b/packages/manager/apps/ips/tsconfig.test.json
@@ -0,0 +1,6 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "module": "CommonJS"
+ }
+}
diff --git a/packages/manager/apps/ips/vite.config.mjs b/packages/manager/apps/ips/vite.config.mjs
new file mode 100644
index 000000000000..f33ab6dc98cd
--- /dev/null
+++ b/packages/manager/apps/ips/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()),
+});