diff --git a/src/elements/MembersPopUp/membersReducer.js b/src/elements/MembersPopUp/membersReducer.js
index 9a045856a9..865deb46d0 100644
--- a/src/elements/MembersPopUp/membersReducer.js
+++ b/src/elements/MembersPopUp/membersReducer.js
@@ -23,8 +23,7 @@ import { groupBy } from 'lodash'
* - activeUser : logged in user data
* - projectInfo : additional information about the project such as ID and Owner of the project
* (data is received from iguazio API).
- * - users : the list of user members (original list from response)
- * - useGroups : the list of user-group members (original list from response)
+ * - projectAuthorizationRoles: the list of project policies from /authorization/projects/{project}/policies
* - membersOriginal : the list of users and user-groups that is used to revert changes
* - members : the list of users and user-groups that is displayed in the table of the `Member` dialog (includes owner)
* - groupedOriginalMembers : grouped members list by their role, which is used to display the number of
diff --git a/src/elements/PanelCredentialsAccessKey/PanelCredentialsAccessKey.jsx b/src/elements/PanelCredentialsAccessKey/PanelCredentialsAccessKey.jsx
index e14799e5d4..3161a09fee 100644
--- a/src/elements/PanelCredentialsAccessKey/PanelCredentialsAccessKey.jsx
+++ b/src/elements/PanelCredentialsAccessKey/PanelCredentialsAccessKey.jsx
@@ -24,7 +24,7 @@ import PropTypes from 'prop-types'
import CheckBox from '../../common/CheckBox/CheckBox'
import Input from '../../common/Input/Input'
-import { PANEL_DEFAULT_ACCESS_KEY } from '../../constants'
+import { PANEL_DEFAULT_ACCESS_KEY, API_TOKEN_TIP } from '../../constants'
import './panelCredentialsAccessKey.scss'
@@ -32,6 +32,7 @@ const PanelCredentialsAccessKey = ({
className = '',
credentialsAccessKey,
frontendSpec,
+ isApiToken = false,
isPanelEditMode = false,
required = false,
setCredentialsAccessKey,
@@ -49,7 +50,7 @@ const PanelCredentialsAccessKey = ({
return (
- {!frontendSpec.ce?.version && (
+ {!frontendSpec.ce?.version && !isApiToken && (
)}
- {credentialsAccessKey !== PANEL_DEFAULT_ACCESS_KEY && (
+ {(isApiToken || credentialsAccessKey !== PANEL_DEFAULT_ACCESS_KEY) && (
{
if (credentialsAccessKey !== event.target.value) {
@@ -87,6 +88,7 @@ const PanelCredentialsAccessKey = ({
isAccessKeyValid: value
}))
}
+ tip={isApiToken ? API_TOKEN_TIP : ''}
value={inputValue}
wrapperClassName="access-key__input"
/>
@@ -99,6 +101,7 @@ PanelCredentialsAccessKey.propTypes = {
className: PropTypes.string,
credentialsAccessKey: PropTypes.string.isRequired,
frontendSpec: PropTypes.object.isRequired,
+ isApiToken: PropTypes.bool,
isPanelEditMode: PropTypes.bool,
required: PropTypes.bool,
setCredentialsAccessKey: PropTypes.func.isRequired,
diff --git a/src/elements/PanelCredentialsAccessKey/panelCredentialsAccessKey.scss b/src/elements/PanelCredentialsAccessKey/panelCredentialsAccessKey.scss
index 64e53a8d03..5c60b92eef 100644
--- a/src/elements/PanelCredentialsAccessKey/panelCredentialsAccessKey.scss
+++ b/src/elements/PanelCredentialsAccessKey/panelCredentialsAccessKey.scss
@@ -4,7 +4,7 @@
height: 64px;
padding: 22px 40px;
- &__input {
+ &__input:not(:first-child) {
margin-left: 20px;
}
}
diff --git a/src/elements/ProjectFunctions/ProjectFunctions.jsx b/src/elements/ProjectFunctions/ProjectFunctions.jsx
index ea8c54617a..48c0d872f7 100644
--- a/src/elements/ProjectFunctions/ProjectFunctions.jsx
+++ b/src/elements/ProjectFunctions/ProjectFunctions.jsx
@@ -32,7 +32,9 @@ import {
FAILED_STATE,
FUNCTION_READY_STATE,
REQUEST_CANCELED,
- RUNNING_STATE
+ RUNNING_STATE,
+ NUCLIO_FUNCTIONS_PATH,
+ IS_MF_MODE
} from '../../constants'
import { fetchApiGateways } from '../../reducers/nuclioReducer'
import { generateNuclioLink } from '../../utils'
@@ -82,7 +84,9 @@ const ProjectFunctions = ({ nuclioStreamsAreEnabled, project }) => {
label: 'Running',
className: RUNNING_STATE,
status: RUNNING_STATE,
- href: generateNuclioLink(`/projects/${params.projectName}/functions`),
+ [IS_MF_MODE ? 'link' : 'href']: generateNuclioLink(
+ `/projects/${params.projectName}/${NUCLIO_FUNCTIONS_PATH}`
+ ),
loading: nuclioStore.loading
},
failed: {
@@ -91,14 +95,18 @@ const ProjectFunctions = ({ nuclioStreamsAreEnabled, project }) => {
label: 'Failed',
status: FAILED_STATE,
className: functionsFailed > 0 ? FAILED_STATE : RUNNING_STATE,
- href: generateNuclioLink(`/projects/${params.projectName}/functions`),
+ [IS_MF_MODE ? 'link' : 'href']: generateNuclioLink(
+ `/projects/${params.projectName}/${NUCLIO_FUNCTIONS_PATH}`
+ ),
loading: nuclioStore.loading
},
apiGateways: {
value: nuclioStore.apiGateways,
label: 'API gateways',
className: RUNNING_STATE,
- href: generateNuclioLink(`/projects/${params.projectName}/api-gateways`),
+ [IS_MF_MODE ? 'link' : 'href']: generateNuclioLink(
+ `/projects/${params.projectName}/api-gateways`
+ ),
loading: nuclioStore.loading
},
...(nuclioStreamsAreEnabled && {
@@ -149,8 +157,8 @@ const ProjectFunctions = ({ nuclioStreamsAreEnabled, project }) => {
? // if nuclio func is generated by MLRun then name only in this case starts with project name and we need to slice it
func.metadata.name.slice(params.projectName.length + 1)
: func.metadata.name,
- href: generateNuclioLink(
- `/projects/${params.projectName}/functions/${func.metadata.name}`
+ [IS_MF_MODE ? 'link' : 'href']: generateNuclioLink(
+ `/projects/${params.projectName}/${NUCLIO_FUNCTIONS_PATH}/${func.metadata.name}`
),
className: 'table-cell_big'
},
@@ -177,7 +185,11 @@ const ProjectFunctions = ({ nuclioStreamsAreEnabled, project }) => {
loading: nuclioStore.loading
}}
footerLinkText="All real-time functions"
- href={generateNuclioLink(`/projects/${params.projectName}/functions`)}
+ {...{
+ [IS_MF_MODE ? 'link' : 'href']: generateNuclioLink(
+ `/projects/${params.projectName}/${NUCLIO_FUNCTIONS_PATH}`
+ )
+ }}
statistics={functions}
subTitle="Recent real-time functions"
table={functionsTable}
diff --git a/src/hooks/nuclioMode.hook.js b/src/hooks/nuclioMode.hook.js
index 88c5fea164..0ba4da3fab 100644
--- a/src/hooks/nuclioMode.hook.js
+++ b/src/hooks/nuclioMode.hook.js
@@ -33,11 +33,11 @@ import { useLayoutEffect, useState } from 'react'
*/
export const useNuclioMode = () => {
- const [mode, setMode] = useState(window.mlrunConfig.nuclioMode)
+ const [mode, setMode] = useState(window?.mlrunConfig?.nuclioMode)
useLayoutEffect(() => {
- if (mode !== window.mlrunConfig.nuclioMode) {
- setMode(window.mlrunConfig.nuclioMode)
+ if (mode !== window?.mlrunConfig?.nuclioMode) {
+ setMode(window?.mlrunConfig?.nuclioMode)
}
}, [mode])
diff --git a/src/hooks/useApiTokenExpiry.js b/src/hooks/useApiTokenExpiry.js
new file mode 100644
index 0000000000..a2eee58f43
--- /dev/null
+++ b/src/hooks/useApiTokenExpiry.js
@@ -0,0 +1,91 @@
+/*
+Copyright 2019 Iguazio Systems Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License") with
+an addition restriction as set forth herein. You may not use this
+file except in compliance with the License. You may obtain a copy of
+the License at http://www.apache.org/licenses/LICENSE-2.0.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the License for the specific language governing
+permissions and limitations under the License.
+
+In addition, you may not use the software for any purposes that are
+illegal under applicable law, and the grant of the foregoing license
+under the Apache 2.0 license is conditioned upon your compliance with
+such restriction.
+*/
+import { useCallback, useMemo, useState, useEffect } from 'react'
+
+import { mainHttpClient } from '../httpClient'
+
+const TOKEN_EXPIRY_WARNING_DAYS = 7
+const DISMISSED_KEY = 'tokenExpiryBannerDismissed'
+
+// Module-level cache — survives component remounts and MF unmount/remount cycles
+let tokenCache = null
+
+const isDismissed = () => sessionStorage.getItem(DISMISSED_KEY) === 'true'
+const persistDismissed = () => sessionStorage.setItem(DISMISSED_KEY, 'true')
+
+const getDaysDifference = targetDate => {
+ const diffTime = new Date(targetDate).getTime() - Date.now()
+ return diffTime / (1000 * 60 * 60 * 24)
+}
+
+const isTokenExpiringSoon = token => {
+ if (!token.expiration) return false
+ return getDaysDifference(token.expiration) <= TOKEN_EXPIRY_WARNING_DAYS
+}
+
+const useApiTokenExpiry = () => {
+ const [tokens, setTokens] = useState(tokenCache)
+ const [dismissed, setDismissedState] = useState(isDismissed)
+
+ useEffect(() => {
+ let cancelled = false
+
+ mainHttpClient
+ .get('/user-secrets/tokens')
+ .then(({ data }) => {
+ if (!cancelled) {
+ const result = data.secret_tokens ?? []
+ tokenCache = result
+ setTokens(result)
+ }
+ })
+ .catch(() => {
+ // On error keep the cached value so the banner doesn't vanish
+ if (!cancelled && tokenCache === null) setTokens([])
+ })
+
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ const hasExpiring = useMemo(
+ () => !dismissed && !!tokens?.some(isTokenExpiringSoon),
+ [tokens, dismissed]
+ )
+
+ const minDaysLeft = useMemo(() => {
+ if (!hasExpiring || !tokens) return 0
+ const expiring = tokens.filter(isTokenExpiringSoon)
+ const min = Math.min(...expiring.map(t => getDaysDifference(t.expiration)))
+ return Math.max(0, Math.ceil(min))
+ }, [tokens, hasExpiring])
+
+ const daysLeftLabel = minDaysLeft <= 1 ? '1 day' : `${minDaysLeft} days`
+
+ const dismiss = useCallback(() => {
+ persistDismissed()
+ setDismissedState(true)
+ }, [])
+
+ return { hasExpiring, daysLeftLabel, dismiss }
+}
+
+export default useApiTokenExpiry
diff --git a/src/httpClient.js b/src/httpClient.js
index a61f075bf2..55ebad86c2 100755
--- a/src/httpClient.js
+++ b/src/httpClient.js
@@ -29,6 +29,13 @@ const headers = {
'Cache-Control': 'no-cache'
}
+/**
+ * Resolve auth bridge provided by igz-ui (host).
+ * - Primary: injected via Module Federation
+ * - Fallback: host global (timing safety)
+ */
+export const getHostAuth = () => window.__mlrunHostServices?.auth || window.__igzAuth || null
+
// serialize a param with an array value as a repeated param, for example:
// { label: ['host', 'owner=admin'] } => 'label=host&label=owner%3Dadmin'
const paramsSerializer = params => qs.stringify(params, { arrayFormat: 'repeat' })
@@ -62,10 +69,68 @@ export const nuclioHttpClient = axios.create({
})
export const iguazioHttpClient = axios.create({
- baseURL: import.meta.env.MODE === 'production' ? '/api' : '/iguazio/api',
+ baseURL:
+ import.meta.env.MODE === 'production'
+ ? import.meta.env.VITE_FEDERATION === 'true'
+ ? '/oris/api'
+ : '/api'
+ : import.meta.env.VITE_FEDERATION === 'true'
+ ? '/oris-mlrun/api'
+ : '/iguazio/api',
headers
})
+/**
+ * Module Federation auth:
+ * token injection and refresh are handled by the igz-ui host
+ */
+
+const attachHostAuth = client => {
+ const auth = getHostAuth()
+ if (!auth) return
+
+ client.interceptors.request.use(config => {
+ const token = auth.getAccessToken?.()
+ if (token) {
+ config.headers = config.headers ?? {}
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+ })
+
+ client.interceptors.response.use(
+ res => res,
+ async err => {
+ const status = err?.response?.status
+ const req = err?.config
+ if (!req) throw err
+
+ if (status === 401 && !req._retry) {
+ req._retry = true
+
+ const token = await auth.refreshAccessToken?.()
+ if (!token) {
+ auth.redirectToLogin?.()
+ throw err
+ }
+
+ req.headers = req.headers ?? {}
+ req.headers.Authorization = `Bearer ${token}`
+
+ return client(req)
+ }
+
+ throw err
+ }
+ )
+}
+
+attachHostAuth(mainHttpClient)
+attachHostAuth(mainHttpClientV2)
+attachHostAuth(functionTemplatesHttpClient)
+attachHostAuth(nuclioHttpClient)
+attachHostAuth(iguazioHttpClient)
+
const getAbortSignal = (controller, abortCallback, timeoutMs) => {
let timeoutId = null
const newController = new AbortController()
diff --git a/src/index.jsx b/src/index.jsx
index 9a141aaa0e..57839eb9e0 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -26,40 +26,23 @@ import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary'
import * as serviceWorker from './serviceWorker'
import { Provider } from 'react-redux'
import toolkitStore from './store/toolkitStore'
-import { HTTP, HTTPS } from './constants'
+import { loadRemoteConfig } from './loadRemoteConfig'
if (!window.location.pathname.includes(import.meta.env.VITE_PUBLIC_URL)) {
window.location.href = import.meta.env.VITE_PUBLIC_URL
}
-fetch(`${import.meta.env.VITE_PUBLIC_URL}/config.json`, { cache: 'no-store' })
- .then(response => response.json())
- .then(config => {
- if (config.nuclioUiUrl) {
- const mlrunProtocol =
- config.nuclioUiUrl.startsWith(HTTP) || config.nuclioUiUrl.startsWith(HTTPS)
- ? ''
- : `${window.location.protocol}//`
-
- window.mlrunConfig = {
- ...config,
- nuclioUiUrl: `${mlrunProtocol}${config.nuclioUiUrl}`
- }
- } else {
- window.mlrunConfig = config
- }
- })
- .then(() => {
- const root = createRoot(document.getElementById('root'))
-
- root.render(
-
-
-
-
-
- )
- })
+loadRemoteConfig().then(() => {
+ const root = createRoot(document.getElementById('root'))
+
+ root.render(
+
+
+
+
+
+ )
+})
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
diff --git a/src/layout/Navbar/navbar.util.jsx b/src/layout/Navbar/navbar.util.jsx
index 0416c906c4..43140def1b 100644
--- a/src/layout/Navbar/navbar.util.jsx
+++ b/src/layout/Navbar/navbar.util.jsx
@@ -24,7 +24,9 @@ import {
DOCUMENTS_PAGE,
LLM_PROMPTS_PAGE,
PROJECT_MONITOR,
- PROJECT_QUICK_ACTIONS_PAGE
+ PROJECT_QUICK_ACTIONS_PAGE,
+ NUCLIO_FUNCTIONS_PATH,
+ IS_MF_MODE
} from '../../constants'
import { generateNuclioLink } from '../../utils'
@@ -125,15 +127,15 @@ export const getLinks = projectName => {
icon: ,
id: 'real-time-functions',
label: 'Real-time functions',
- link: generateNuclioLink(`${pathname}/functions`),
- externalLink: true
+ link: generateNuclioLink(`${pathname}/${NUCLIO_FUNCTIONS_PATH}`),
+ externalLink: !IS_MF_MODE
},
{
icon: ,
id: 'api-gateways',
label: 'API gateways',
link: generateNuclioLink(`${pathname}/api-gateways`),
- externalLink: true
+ externalLink: !IS_MF_MODE
},
{
icon: ,
diff --git a/src/layout/Page/Page.jsx b/src/layout/Page/Page.jsx
index 3edc0d4fcb..2c73dcb9cd 100644
--- a/src/layout/Page/Page.jsx
+++ b/src/layout/Page/Page.jsx
@@ -26,6 +26,8 @@ import ModalContainer from 'react-modal-promise'
import Sidebar from '../../nextGenComponents/shared/Sidebar'
import { SidebarInset, SidebarProvider } from 'igz-controls/nextGenComponents'
+import HostLeaveGuard from '../../common/HostLeaveGuard/HostLeaveGuard'
+import TokenExpiryBanner from '../../common/TokenExpiryBanner/TokenExpiryBanner'
import YamlModal from '../../common/YamlModal/YamlModal'
import { Loader } from 'igz-controls/components'
@@ -99,11 +101,13 @@ const Page = () => {
return (
+
{projectName && }
<>
}>
+
{isProjectsFetched ? : }
diff --git a/src/lazyWithRetry.js b/src/lazyWithRetry.js
index f44786a6bb..4be065b970 100644
--- a/src/lazyWithRetry.js
+++ b/src/lazyWithRetry.js
@@ -19,26 +19,33 @@ such restriction.
*/
import { lazy } from 'react'
// a function to retry loading a chunk to avoid chunk load error for out of date code
-export const lazyRetry = componentImport =>
- lazy(() => {
+export const lazyRetry = (componentImport, name) => {
+ if (!name) {
+ throw new Error('lazyRetry requires a name for the component being imported')
+ }
+
+ return lazy(() => {
return new Promise((resolve, reject) => {
// check if the window has already been refreshed
- const hasRefreshed = JSON.parse(
- window.sessionStorage.getItem('retry-lazy-refreshed') || 'false'
- )
+ const componentStorageKey = `retry-lazy-refreshed-${name}`
+ const hasRefreshed = JSON.parse(window.sessionStorage.getItem(componentStorageKey) || 'false')
// try to import the component
componentImport()
.then(component => {
- window.sessionStorage.setItem('retry-lazy-refreshed', 'false') // success so reset the refresh
+ window.sessionStorage.setItem(componentStorageKey, 'false') // success so reset the refresh
resolve(component)
})
.catch(error => {
+ window.sessionStorage.setItem(componentStorageKey, 'true')
if (!hasRefreshed) {
// not been refreshed yet
window.sessionStorage.setItem('retry-lazy-refreshed', 'true') // we are now going to refresh
return window.location.reload() // refresh the page
}
- reject(error) // Default error behaviour as already tried refresh
+ // eslint-disable-next-line no-console
+ console.error(`Failed to load component ${name} after retrying`, error)
+ reject(error) // Default error behavior as already tried refresh
})
})
})
+}
diff --git a/src/loadRemoteConfig.js b/src/loadRemoteConfig.js
new file mode 100644
index 0000000000..febebc05d6
--- /dev/null
+++ b/src/loadRemoteConfig.js
@@ -0,0 +1,60 @@
+/*
+Copyright 2019 Iguazio Systems Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License") with
+an addition restriction as set forth herein. You may not use this
+file except in compliance with the License. You may obtain a copy of
+the License at http://www.apache.org/licenses/LICENSE-2.0.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the License for the specific language governing
+permissions and limitations under the License.
+
+In addition, you may not use the software for any purposes that are
+illegal under applicable law, and the grant of the foregoing license
+under the Apache 2.0 license is conditioned upon your compliance with
+such restriction.
+*/
+
+import { HTTP, HTTPS } from './constants'
+
+const withProtocol = url => {
+ if (!url || url.startsWith(HTTP) || url.startsWith(HTTPS)) return url
+ return `${window.location.protocol}//${url.replace(/^\/+/, '')}`
+}
+
+export const loadRemoteConfig = async (url, services = {}) => {
+ /**
+ * Store host-provided services (auth bridge from igz-ui)
+ */
+ if (services && Object.keys(services).length > 0) {
+ window.__mlrunHostServices = services
+ }
+
+ // Priority 1: Use config injected by the Host (igz-ui)
+ if (window.mlrunConfig && Object.keys(window.mlrunConfig).length > 0) {
+ window.mlrunConfig.nuclioUiUrl = withProtocol(window.mlrunConfig.nuclioUiUrl)
+ window.mlrunConfig.nuclioRemoteEntryUrl = withProtocol(window.mlrunConfig.nuclioRemoteEntryUrl)
+ return
+ }
+
+ // Priority 2: Standalone Fallback (Local Dev)
+ try {
+ const configPath = `${url ?? import.meta.env.VITE_PUBLIC_URL}/config.json`
+ const response = await fetch(configPath, { cache: 'no-store' })
+ if (!response.ok) throw new Error(response.status)
+
+ const config = await response.json()
+ const uiUrl = withProtocol(config.nuclioUiUrl)
+
+ window.mlrunConfig = {
+ ...config,
+ nuclioUiUrl: uiUrl,
+ nuclioRemoteEntryUrl: withProtocol(config.nuclioRemoteEntryUrl || uiUrl)
+ }
+ } catch (err) {
+ throw new Error('[mlrun-ui] Config load failed. Falling back to Host injection.', err)
+ }
+}
diff --git a/src/main.jsx b/src/main.jsx
new file mode 100644
index 0000000000..40ada1bd44
--- /dev/null
+++ b/src/main.jsx
@@ -0,0 +1,20 @@
+import App from './App'
+import { Provider } from 'react-redux'
+import store from './store/toolkitStore'
+import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary'
+// Eagerly initialize react-text-mask in the entry chunk so its CJS/UMD module
+// is resolved against React before any lazy chunks try to reference it.
+// TODO: remove this import when custom react-text-mask will be included in our codebase (in progress) or when https://github.com/rollup/rollup/issues/6296 fixed (deps of vite)
+import 'react-text-mask'
+
+const RemoteApp = () => {
+ return (
+
+
+
+
+
+ )
+}
+
+export default RemoteApp
diff --git a/src/nextGenComponents/pages/ApplicationsPage/applicationsPage.util.test.js b/src/nextGenComponents/pages/ApplicationsPage/applicationsPage.util.test.js
index 973a00ea14..8cabc4574e 100644
--- a/src/nextGenComponents/pages/ApplicationsPage/applicationsPage.util.test.js
+++ b/src/nextGenComponents/pages/ApplicationsPage/applicationsPage.util.test.js
@@ -17,7 +17,7 @@ illegal under applicable law, and the grant of the foregoing license
under the Apache 2.0 license is conditioned upon your compliance with
such restriction.
*/
-// eslint-disable-next-line import/named
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
import { APPLICATION_STATUS, APPLICATION_STATUS_OPTIONS } from './applications.constants'
diff --git a/src/nextGenComponents/shared/Sidebar/navbarList.util.jsx b/src/nextGenComponents/shared/Sidebar/navbarList.util.jsx
index 24eb27a26a..1afc4f19cc 100644
--- a/src/nextGenComponents/shared/Sidebar/navbarList.util.jsx
+++ b/src/nextGenComponents/shared/Sidebar/navbarList.util.jsx
@@ -39,9 +39,10 @@ import {
PROJECTS_PAGE_PATH,
PROJECT_MONITOR,
REAL_TIME_PIPELINES_TAB,
- REAL_TIME_FUNCTIONS_PAGE,
+ NUCLIO_FUNCTIONS_PATH,
SCHEDULE_TAB,
- ALERTS_PAGE_PATH
+ ALERTS_PAGE_PATH,
+ IS_MF_MODE
} from '../../../constants'
import { generateNuclioLink } from '../../../utils'
@@ -158,16 +159,16 @@ export const getLinks = projectName => {
label: 'Nuclio',
nestedLinks: [
{
- id: REAL_TIME_FUNCTIONS_PAGE,
+ id: NUCLIO_FUNCTIONS_PATH,
label: 'Real-time functions',
- link: generateNuclioLink(`${pathname}/${FUNCTIONS_PAGE_PATH}`),
- externalLink: true
+ link: generateNuclioLink(`${pathname}/${NUCLIO_FUNCTIONS_PATH}`),
+ externalLink: !IS_MF_MODE
},
{
id: API_GATEWAYS_PAGE,
label: 'API gateways',
link: generateNuclioLink(`${pathname}/${API_GATEWAYS_PAGE}`),
- externalLink: true
+ externalLink: !IS_MF_MODE
}
]
},
diff --git a/src/reducers/featureStoreReducer.js b/src/reducers/featureStoreReducer.js
index 53deac56ac..a493d4fdda 100644
--- a/src/reducers/featureStoreReducer.js
+++ b/src/reducers/featureStoreReducer.js
@@ -24,7 +24,6 @@ import {
} from '../utils/getUniqueIdentifier'
import featureStoreApi from '../api/featureStore-api'
import { FORBIDDEN_ERROR_STATUS_CODE } from 'igz-controls/constants'
-import { PANEL_DEFAULT_ACCESS_KEY } from '../constants'
import { REDISNOSQL } from '../components/FeatureSetsPanel/FeatureSetsPanelTargetStore/featureSetsPanelTargetStore.util'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { hideLoading, showLoading } from './redux.util'
@@ -67,7 +66,7 @@ const initialState = {
loading: false,
newFeatureSet: {
credentials: {
- access_key: PANEL_DEFAULT_ACCESS_KEY
+ access_key: 'default' // TODO should be conditional based on env for igz3 PANEL_DEFAULT_ACCESS_KEY
},
metadata: {
name: '',
diff --git a/src/reducers/jobReducer.js b/src/reducers/jobReducer.js
index 12df491dd7..7ffe9f70e3 100644
--- a/src/reducers/jobReducer.js
+++ b/src/reducers/jobReducer.js
@@ -27,7 +27,8 @@ import {
LABELS_FILTER,
NAME_FILTER,
STATUS_FILTER,
- TYPE_FILTER
+ TYPE_FILTER,
+ IS_MF_MODE
} from '../constants'
import { largeResponseCatchHandler } from '../utils/largeResponseCatchHandler'
import functionsApi from '../api/functions-api'
@@ -65,11 +66,13 @@ const initialState = {
}
},
function: {
- metadata: {
- credentials: {
- access_key: ''
+ ...(!IS_MF_MODE && {
+ metadata: {
+ credentials: {
+ access_key: ''
+ }
}
- },
+ }),
spec: {
env: [],
node_selector: {},
diff --git a/src/setupProxy.js b/src/setupProxy.js
deleted file mode 100644
index 80056e64d8..0000000000
--- a/src/setupProxy.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
-Copyright 2019 Iguazio Systems Ltd.
-
-Licensed under the Apache License, Version 2.0 (the "License") with
-an addition restriction as set forth herein. You may not use this
-file except in compliance with the License. You may obtain a copy of
-the License at http://www.apache.org/licenses/LICENSE-2.0.
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-implied. See the License for the specific language governing
-permissions and limitations under the License.
-
-In addition, you may not use the software for any purposes that are
-illegal under applicable law, and the grant of the foregoing license
-under the Apache 2.0 license is conditioned upon your compliance with
-such restriction.
-*/
-const { createProxyMiddleware } = require('http-proxy-middleware')
-
-module.exports = function (app) {
- app.use(
- '/api',
- createProxyMiddleware({
- target: import.meta.env.VITE_MLRUN_API_URL,
- changeOrigin: true,
- headers: {
- Connection: 'keep-alive'
- },
- onProxyReq: function (proxyReq) {
- proxyReq.setHeader('x-v3io-session-key', import.meta.env.VITE_MLRUN_V3IO_ACCESS_KEY)
- proxyReq.setHeader('x-remote-user', 'admin')
- }
- })
- )
-
- if (import.meta.env.VITE_NUCLIO_API_URL) {
- app.use(
- '/nuclio',
- createProxyMiddleware({
- target: import.meta.env.VITE_NUCLIO_API_URL,
- pathRewrite: {
- '^/nuclio': ''
- },
- changeOrigin: true
- })
- )
- }
-
- if (import.meta.env.VITE_IGUAZIO_API_URL) {
- app.use(
- '/iguazio',
- createProxyMiddleware({
- target: import.meta.env.VITE_IGUAZIO_API_URL,
- pathRewrite: {
- '^/iguazio': ''
- },
- changeOrigin: true
- })
- )
- }
-
- if (import.meta.env.VITE_FUNCTION_CATALOG_URL) {
- app.use(
- '/function-catalog',
- createProxyMiddleware({
- target: import.meta.env.VITE_FUNCTION_CATALOG_URL,
- pathRewrite: {
- '^/function-catalog': ''
- },
- changeOrigin: true
- })
- )
- }
-}
diff --git a/src/utils/createApplicationContent.jsx b/src/utils/createApplicationContent.jsx
index d8422185dc..e9be466a5c 100644
--- a/src/utils/createApplicationContent.jsx
+++ b/src/utils/createApplicationContent.jsx
@@ -22,7 +22,7 @@ import { capitalize } from 'lodash'
import { formatDatetime } from 'igz-controls/utils/datetime.util'
import { generateNuclioLink } from './parseUri'
import { saveAndTransformSearchParams } from 'igz-controls/utils/filter.util'
-import { MONITORING_APP_PAGE } from '../constants'
+import { IS_MF_MODE, MONITORING_APP_PAGE, NUCLIO_FUNCTIONS_PATH } from '../constants'
export const createApplicationContent = (application, projectName) => {
const identifierUnique = 'identifierUnique.' + application.name + application.application_class
@@ -105,8 +105,10 @@ export const createApplicationContent = (application, projectName) => {
value: application.name,
className: 'table-cell-2',
getLink: () =>
- generateNuclioLink(`/projects/${projectName}/functions/${nuclioFunctionName}`),
- linkIsExternal: true,
+ generateNuclioLink(
+ `/projects/${projectName}/${NUCLIO_FUNCTIONS_PATH}/${nuclioFunctionName}`
+ ),
+ linkIsExternal: !IS_MF_MODE,
showStatus: true
}
]
diff --git a/src/utils/createConsumerGroupsContent.js b/src/utils/createConsumerGroupsContent.js
index db9ea22f54..14781c18c4 100644
--- a/src/utils/createConsumerGroupsContent.js
+++ b/src/utils/createConsumerGroupsContent.js
@@ -19,6 +19,7 @@ such restriction.
*/
import { generateNuclioLink } from './parseUri'
import { getV3ioStreamIdentifier } from './getUniqueIdentifier'
+import { IS_MF_MODE, NUCLIO_FUNCTIONS_PATH } from '../constants'
const createConsumerGroupsContent = (content, params) => {
return content.map(contentItem => {
@@ -46,10 +47,10 @@ const createConsumerGroupsContent = (content, params) => {
value: contentItem.functionName,
getLink: () => {
return generateNuclioLink(
- `/projects/${params.projectName}/functions/${contentItem.functionName}`
+ `/projects/${params.projectName}/${NUCLIO_FUNCTIONS_PATH}/${contentItem.functionName}`
)
},
- linkIsExternal: true,
+ linkIsExternal: !IS_MF_MODE,
className: 'table-cell-1'
}
}
diff --git a/src/utils/createRealTimePipelinesContent.js b/src/utils/createRealTimePipelinesContent.js
index 5121112a3e..4375fde20e 100644
--- a/src/utils/createRealTimePipelinesContent.js
+++ b/src/utils/createRealTimePipelinesContent.js
@@ -19,7 +19,13 @@ such restriction.
*/
import { formatDatetime } from 'igz-controls/utils/datetime.util'
-import { DETAILS_MODEL_ENDPOINTS_TAB, MODELS_PAGE, REAL_TIME_PIPELINES_TAB } from '../constants'
+import {
+ DETAILS_MODEL_ENDPOINTS_TAB,
+ IS_MF_MODE,
+ MODELS_PAGE,
+ NUCLIO_FUNCTIONS_PATH,
+ REAL_TIME_PIPELINES_TAB
+} from '../constants'
import { typesOfJob } from './jobs.util'
import { generateNuclioLink } from './parseUri'
@@ -50,9 +56,11 @@ const createRealTimePipelinesContent = (pipelines, projectName) =>
className: 'table-cell-2',
showStatus: true,
showTag: true,
- linkIsExternal: true,
+ linkIsExternal: !IS_MF_MODE,
getLink: () =>
- generateNuclioLink(`/projects/${projectName}/functions/${nuclioFunctionName}`)
+ generateNuclioLink(
+ `/projects/${projectName}/${NUCLIO_FUNCTIONS_PATH}/${nuclioFunctionName}`
+ )
},
{
id: `topology.${pipeline.ui.identifierUnique}`,
diff --git a/src/utils/getArtifactPreview.jsx b/src/utils/getArtifactPreview.jsx
index 5748253458..86b4abc58a 100644
--- a/src/utils/getArtifactPreview.jsx
+++ b/src/utils/getArtifactPreview.jsx
@@ -191,7 +191,7 @@ export const fetchArtifactPreviewFromPath = async (
{
data: {
content: `The artifact is too large to ${
- fileSize > artifactLimits.max_download_size
+ fileSize > artifactLimits?.max_download_size
? `download. Go to ${path} to retrieve the data, or use mlrun api/sdk project.get_artifact('${artifact.db_key || artifact.name}').to_dataitem().get()`
: 'preview, use the download option instead'
}`
diff --git a/src/utils/getJobLogs.util.js b/src/utils/getJobLogs.util.js
index c78b971249..7d41761d6c 100644
--- a/src/utils/getJobLogs.util.js
+++ b/src/utils/getJobLogs.util.js
@@ -34,7 +34,7 @@ export const getJobLogs = (
dispatch(fetchJobLogs({ id, project, attempt, signal }))
.unwrap()
.then(res => {
- const reader = res.body?.getReader()
+ const reader = res.data?.getReader()
if (reader) {
const decoder = new TextDecoder()
diff --git a/src/utils/nuclio.remotes.utils.js b/src/utils/nuclio.remotes.utils.js
new file mode 100644
index 0000000000..90bf9f0365
--- /dev/null
+++ b/src/utils/nuclio.remotes.utils.js
@@ -0,0 +1,69 @@
+/*
+Copyright 2019 Iguazio Systems Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License") with
+an addition restriction as set forth herein. You may not use this
+file except in compliance with the License. You may obtain a copy of
+the License at http://www.apache.org/licenses/LICENSE-2.0.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the License for the specific language governing
+permissions and limitations under the License.
+
+In addition, you may not use the software for any purposes that are
+illegal under applicable law, and the grant of the foregoing license
+under the Apache 2.0 license is conditioned upon your compliance with
+such restriction.
+*/
+import { loadRemote, registerRemotes } from '@module-federation/runtime'
+
+let registerPromise = null
+
+const ensureNuclioRemote = async () => {
+ if (registerPromise) return registerPromise
+
+ const config = window?.mlrunConfig
+ let remoteEntryUrl = config?.nuclioRemoteEntryUrl
+
+ if (!remoteEntryUrl) {
+ throw new Error('[MF] Missing window.mlrunConfig.nuclioRemoteEntryUrl')
+ }
+
+ if (window.location.hostname !== 'localhost') {
+ remoteEntryUrl = `${remoteEntryUrl.replace(/\/$/, '')}/nuclio-ui`
+ } else {
+ remoteEntryUrl = remoteEntryUrl.replace(/\/$/, '')
+ }
+
+ registerPromise = (async () => {
+ try {
+ registerRemotes([
+ {
+ name: 'nuclio',
+ entry: `${remoteEntryUrl.replace(/\/$/, '')}/remoteEntry.js`,
+ type: 'module',
+ shareScope: 'default'
+ }
+ ])
+ } catch (err) {
+ registerPromise = null
+ throw err
+ }
+ })()
+
+ return registerPromise
+}
+
+const loadNuclioApp = async () => {
+ await ensureNuclioRemote()
+ const module = await loadRemote('nuclio/App')
+
+ if (!module) throw new Error('[MF] Failed to load Nuclio application')
+
+ const component = module.default?.default || module.default || module
+ return { default: component }
+}
+
+export { ensureNuclioRemote, loadNuclioApp }
diff --git a/src/utils/parseUri.js b/src/utils/parseUri.js
index 26167d094d..b15de1ef1d 100644
--- a/src/utils/parseUri.js
+++ b/src/utils/parseUri.js
@@ -24,7 +24,8 @@ import {
MONITOR_JOBS_TAB,
FILES_PAGE,
DATASETS_PAGE,
- DOCUMENTS_PAGE
+ DOCUMENTS_PAGE,
+ IS_MF_MODE
} from '../constants'
/**
@@ -117,10 +118,14 @@ const generateLinkPath = (uri = '') => {
}
const generateNuclioLink = pathname => {
- const linkUrl = new URL(`${window.mlrunConfig.nuclioUiUrl}${pathname}`)
+ if (IS_MF_MODE) return pathname
- if (window.location.origin !== window.mlrunConfig.nuclioUiUrl) {
- linkUrl.searchParams.set?.('origin', window.location.origin)
+ const base = window.mlrunConfig?.nuclioUiUrl || window.location.origin
+ const cleanPath = pathname.startsWith('/') ? pathname : `/${pathname}`
+ const linkUrl = new URL(`${base}${cleanPath}`)
+
+ if (window.location.origin !== base) {
+ linkUrl.searchParams.set('origin', window.location.origin)
}
return linkUrl.toString()
diff --git a/src/utils/projectAuth.util.js b/src/utils/projectAuth.util.js
new file mode 100644
index 0000000000..ec99b8b0b5
--- /dev/null
+++ b/src/utils/projectAuth.util.js
@@ -0,0 +1,53 @@
+/*
+Copyright 2019 Iguazio Systems Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License") with
+an addition restriction as set forth herein. You may not use this
+file except in compliance with the License. You may obtain a copy of
+the License at http://www.apache.org/licenses/LICENSE-2.0.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the License for the specific language governing
+permissions and limitations under the License.
+
+In addition, you may not use the software for any purposes that are
+illegal under applicable law, and the grant of the foregoing license
+under the Apache 2.0 license is conditioned upon your compliance with
+such restriction.
+*/
+import projectsIguazioApi from '../api/projects-iguazio-api'
+
+const WRITE_ROLES = ['Owner', 'Admin', 'Editor']
+
+export const getActiveUsername = async () => {
+ const response = await projectsIguazioApi.getActiveUser()
+ return response.data.metadata?.username
+}
+
+export const checkProjectWriteAccess = async (projectName, activeUsername = null) => {
+ if (import.meta.env.VITE_FEDERATION === 'true') {
+ if (!activeUsername) {
+ activeUsername = await getActiveUsername()
+ }
+
+ const policiesResponse = await projectsIguazioApi.getProjectPolicies(projectName)
+ const policies = policiesResponse.data.items || []
+ return policies.some(
+ policy =>
+ WRITE_ROLES.includes(policy.spec.displayName) &&
+ policy.status?.assignedMembers?.some(member => member.id === activeUsername)
+ )
+ } else {
+ return projectsIguazioApi
+ .getProjectOwnerVisibility(projectName)
+ .then(() => true)
+ .catch(() =>
+ projectsIguazioApi
+ .getProjectWorkflowsUpdateAuthorization(projectName)
+ .then(() => true)
+ .catch(() => false)
+ )
+ }
+}
diff --git a/vite.config.mjs b/vite.config.mjs
index b7bb90b568..f60034369d 100644
--- a/vite.config.mjs
+++ b/vite.config.mjs
@@ -1,50 +1,40 @@
import commonjs from 'vite-plugin-commonjs'
import eslint from 'vite-plugin-eslint'
+import { federation } from '@module-federation/vite'
import path from 'node:path'
import react from '@vitejs/plugin-react-swc'
import svgr from 'vite-plugin-svgr'
import { defineConfig, loadEnv } from 'vite'
-export default defineConfig(({ mode }) => {
+import { loadMlrunProxyConfig } from './config/loadDevProxyConfig.js'
+import { dependencies } from './package.json'
+
+export default defineConfig(async ({ mode }) => {
const env = loadEnv(mode, path.resolve(process.cwd()), '')
+ const mlrunProxyConfig = await loadMlrunProxyConfig(mode)
+
+ const federationPlugin =
+ env.VITE_FEDERATION === 'true'
+ ? federation({
+ filename: 'remoteEntry.js',
+ name: 'mlrun',
+ exposes: {
+ './loadRemoteConfig': './src/loadRemoteConfig.js',
+ './app': './src/main.jsx'
+ },
+ shared: {
+ react: { requiredVersion: dependencies.react, singleton: true },
+ 'react-dom': { requiredVersion: dependencies['react-dom'], singleton: true }
+ }
+ })
+ : null
return {
- plugins: [commonjs(), react(), svgr(), eslint({ failOnError: false })],
+ plugins: [commonjs(), react(), federationPlugin, svgr(), eslint({ failOnError: false })],
base: env.NODE_ENV === 'production' ? env.VITE_PUBLIC_URL : '/',
server: {
proxy: {
- '/api': env.VITE_MLRUN_API_URL
- ? {
- target: env.VITE_MLRUN_API_URL,
- changeOrigin: true,
- headers: {
- Connection: 'keep-alive',
- 'x-v3io-session-key': env.VITE_MLRUN_V3IO_ACCESS_KEY,
- 'x-remote-user': 'admin'
- }
- }
- : undefined,
- '/nuclio': env.VITE_NUCLIO_API_URL
- ? {
- target: env.VITE_NUCLIO_API_URL,
- changeOrigin: true,
- rewrite: path => path.replace(/^\/nuclio/, '')
- }
- : undefined,
- '/iguazio': env.VITE_IGUAZIO_API_URL
- ? {
- target: env.VITE_IGUAZIO_API_URL,
- changeOrigin: true,
- rewrite: path => path.replace(/^\/iguazio/, '')
- }
- : undefined,
- '/function-catalog': env.VITE_FUNCTION_CATALOG_URL
- ? {
- target: env.VITE_FUNCTION_CATALOG_URL,
- changeOrigin: true,
- rewrite: path => path.replace(/^\/function-catalog/, '')
- }
- : undefined
+ ...mlrunProxyConfig(env)
},
fs: {
strict: false
@@ -84,6 +74,7 @@ export default defineConfig(({ mode }) => {
force: true
},
build: {
+ target: 'esnext',
sourcemap: true,
outDir: 'build',
chunkSizeWarningLimit: 3000