diff --git a/.changeset/dull-apricots-happen.md b/.changeset/dull-apricots-happen.md new file mode 100644 index 00000000000..540704b858d --- /dev/null +++ b/.changeset/dull-apricots-happen.md @@ -0,0 +1,77 @@ +--- +"@wso2is/admin.identity-verification-providers.v1": minor +"@wso2is/admin.remote-repository-configuration.v1": minor +"@wso2is/admin.alternative-login-identifier.v1": minor +"@wso2is/admin.authentication-flow-builder.v1": minor +"@wso2is/admin.organization-discovery.v1": minor +"@wso2is/admin.application-templates.v1": minor +"@wso2is/admin.server-configurations.v1": minor +"@wso2is/admin.saml2-configuration.v1": minor +"@wso2is/admin.username-validation.v1": minor +"@wso2is/admin.wsfed-configuration.v1": minor +"@wso2is/admin.identity-providers.v1": minor +"@wso2is/admin.session-management.v1": minor +"@wso2is/admin.workflow-approvals.v1": minor +"@wso2is/admin.application-roles.v1": minor +"@wso2is/admin.remote-userstores.v1": minor +"@wso2is/admin.console-settings.v1": minor +"@wso2is/admin.email-management.v1": minor +"@wso2is/admin.email-providers.v1": minor +"@wso2is/admin.email-templates.v1": minor +"@wso2is/admin.private-key-jwt.v1": minor +"@wso2is/admin.administrators.v1": minor +"@wso2is/admin.authentication.v1": minor +"@wso2is/admin.api-resources.v1": minor +"@wso2is/admin.api-resources.v2": minor +"@wso2is/admin.authorization.v1": minor +"@wso2is/admin.email-and-sms.v1": minor +"@wso2is/admin.impersonation.v1": minor +"@wso2is/admin.login-flow.ai.v1": minor +"@wso2is/admin.organizations.v1": minor +"@wso2is/admin.sms-providers.v1": minor +"@wso2is/admin.template-core.v1": minor +"@wso2is/admin.applications.v1": minor +"@wso2is/admin.certificates.v1": minor +"@wso2is/admin.feature-gate.v1": minor +"@wso2is/admin.org-insights.v1": minor +"@wso2is/admin.provisioning.v1": minor +"@wso2is/admin.subscription.v1": minor +"@wso2is/admin.branding.ai.v1": minor +"@wso2is/admin.connections.v1": minor +"@wso2is/admin.oidc-scopes.v1": minor +"@wso2is/admin.extensions.v1": minor +"@wso2is/admin.userstores.v1": minor +"@wso2is/admin.validation.v1": minor +"@wso2is/common.branding.v1": minor +"@wso2is/admin.branding.v1": minor +"@wso2is/admin.actions.v1": minor +"@wso2is/admin.secrets.v1": minor +"@wso2is/admin.tenants.v1": minor +"@wso2is/admin.claims.v1": minor +"@wso2is/admin.groups.v1": minor +"@wso2is/admin.server.v1": minor +"@wso2is/react-components": minor +"@wso2is/admin.roles.v1": minor +"@wso2is/admin.roles.v2": minor +"@wso2is/admin.users.v1": minor +"@wso2is/admin.administration.v1": minor +"@wso2is/admin.core.v1": minor +"@wso2is/admin.home.v1": minor +"@wso2is/admin.logs.v1": minor +"@wso2is/access-control": minor +"@wso2is/common.ai.v1": minor +"@wso2is/dynamic-forms": minor +"@wso2is/unit-testing": minor +"@wso2is/identity-apps-core": minor +"@wso2is/validation": minor +"@wso2is/myaccount": minor +"@wso2is/forms": minor +"@wso2is/theme": minor +"@wso2is/console": minor +"@wso2is/core": minor +"@wso2is/form": minor +"@wso2is/i18n": minor +"@wso2is/features": minor +--- + +Introduce Multi-Tenancy feature diff --git a/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 b/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 index 77e2c4444ab..2085016368c 100644 --- a/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 +++ b/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 @@ -1571,6 +1571,36 @@ } }, {% endif %} + {% if console.tenants is defined %} + "tenants": { + "disabledFeatures": [ + {% if console.tenants.disabled_features is defined %} + {% for feature in console.tenants.disabled_features %} + "{{ feature }}"{{ "," if not loop.last }} + {% endfor %} + {% endif %} + ], + "enabled": {% if console.tenants.enabled is defined %} {{ console.tenants.enabled }}, + {% else %} true, + {% endif %} + "scopes": { + {% if console.tenants.scopes is defined %} + {% for operation, scopes in console.tenants.scopes.items() %} + "{{ operation }}": [ + {% for scope in scopes %} + "{{ scope }}"{{ "," if not loop.last }} + {% endfor %} + ]{{ "," if not loop.last }} + {% endfor %} + {% else %} + "create": [], + "read": [], + "update": [], + "delete": [] + {% endif %} + } + }, + {% endif %} {% if console.try_it is defined %} "tryIt": { "disabledFeatures": [ diff --git a/apps/console/src/app.scss b/apps/console/src/app.scss new file mode 100644 index 00000000000..0ed81900f0e --- /dev/null +++ b/apps/console/src/app.scss @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); 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. + */ + +:root { + // TODO: Move this to `@oxygen` and set the values from MUI theme spacing. + --oxygen-spacing: 8px; + --oxygen-spacing-1: calc(var(--oxygen-spacing) * 1); + --oxygen-spacing-2: calc(var(--oxygen-spacing) * 2); + --oxygen-spacing-3: calc(var(--oxygen-spacing) * 3); + --oxygen-spacing-4: calc(var(--oxygen-spacing) * 4); + --oxygen-spacing-5: calc(var(--oxygen-spacing) * 5); + --oxygen-spacing-6: calc(var(--oxygen-spacing) * 6); + + --wso2is-admin-modal-form-max-width: 600px; + --wso2is-admin-form-max-width: 750px; +} diff --git a/apps/console/src/app.tsx b/apps/console/src/app.tsx index 100969dc230..9f54f75c946 100755 --- a/apps/console/src/app.tsx +++ b/apps/console/src/app.tsx @@ -67,6 +67,7 @@ import { Dispatch } from "redux"; import "moment/locale/si"; import "moment/locale/fr"; import { getBaseRoutes } from "./configs/routes"; +import "./app.scss"; /** * Main App component. diff --git a/apps/console/src/configs/routes.tsx b/apps/console/src/configs/routes.tsx index e8b4224728b..aebd597f3c6 100644 --- a/apps/console/src/configs/routes.tsx +++ b/apps/console/src/configs/routes.tsx @@ -1056,34 +1056,6 @@ export const getAppViewRoutes = (): RouteInterface[] => { { category: "extensions:manage.sidePanel.categories.settings", children: [ - { - component: lazy(() => - import("@wso2is/admin.server.v1/pages/admin-session-advisory-banner-page") - ), - exact: true, - icon: { - icon: getSidePanelIcons().childIcon - }, - id: "admin-session-advisory-banner-edit", - name: "Admin Session Advisory Banner", - path: AppConstants.getPaths().get("ADMIN_ADVISORY_BANNER_EDIT"), - protected: true, - showOnSidePanel: false - }, - { - component: lazy(() => - import("@wso2is/admin.server.v1/pages/remote-logging-page") - ), - exact: true, - icon: { - icon: getSidePanelIcons().childIcon - }, - id: "remote-logging", - name: "Remote Logging", - path: AppConstants.getPaths().get("REMOTE_LOGGING"), - protected: true, - showOnSidePanel: false - }, { component: lazy(() => import("@wso2is/admin.server.v1/pages/internal-notification-sending-page") @@ -1421,19 +1393,52 @@ export const getFullScreenViewRoutes = (): RouteInterface[] => { * @returns */ export const getDefaultLayoutRoutes = (): RouteInterface[] => { - const routes: RouteInterface[] = []; - - routes.push({ - component: lazy(() => import("@wso2is/admin.core.v1/pages/privacy")), - icon: null, - id: "privacy", - name: "console:common.sidePanel.privacy", - path: AppConstants.getPaths().get("PRIVACY"), - protected: true, - showOnSidePanel: false - }); - - return routes; + return [ + { + component: lazy(() => import("@wso2is/admin.core.v1/pages/privacy")), + icon: null, + id: "privacy", + name: "console:common.sidePanel.privacy", + path: AppConstants.getPaths().get("PRIVACY"), + protected: true, + showOnSidePanel: false + }, + { + children: [ + { + component: lazy(() => import("@wso2is/admin.tenants.v1/pages/system-settings-page")), + exact: true, + icon: null, + id: "systemSettings", + order: 2, + path: AppConstants.getPaths().get("SYSTEM_SETTINGS"), + protected: true, + showOnSidePanel: true + }, + { + component: lazy(() => import("@wso2is/admin.tenants.v1/pages/edit-tenant-page")), + exact: true, + icon: null, + id: "editRootOrganization", + order: 1, + path: AppConstants.getPaths().get("EDIT_TENANT"), + protected: true, + showOnSidePanel: false + } + ], + component: lazy(() => import("@wso2is/admin.tenants.v1/pages/tenants-page")), + exact: true, + icon: { + icon: getSidePanelIcons().administrators + }, + id: "tenants", + name: "console:common.sidePanel.tenants", + order: 1, + path: AppConstants.getPaths().get("TENANTS"), + protected: true, + showOnSidePanel: false + } + ]; }; /** @@ -1530,17 +1535,19 @@ export const getAuthLayoutRoutes = (): RouteInterface[] => { * * @returns */ -const getLayoutAssignedToRoutes = (routes: RouteInterface[], layout: FunctionComponent) => { - let modifiedRoutes: RouteInterface[] = [ ...routes ]; - - modifiedRoutes = modifiedRoutes.map((route: RouteInterface) => { - return { +const getLayoutAssignedToRoutes = (routes: RouteInterface[], layout: FunctionComponent): RouteInterface[] => { + return routes.map((route: RouteInterface) => { + const modifiedRoute: RouteInterface = { ...route, component: layout }; - }); - return modifiedRoutes; + if (route.children) { + modifiedRoute.children = getLayoutAssignedToRoutes(route.children, layout); + } + + return modifiedRoute; + }); }; /** @@ -1551,7 +1558,6 @@ const getLayoutAssignedToRoutes = (routes: RouteInterface[], layout: FunctionCom export const getAppLayoutRoutes = (): RouteInterface[] => { return [ ...getLayoutAssignedToRoutes(getAuthLayoutRoutes(), AuthLayout), - ...getLayoutAssignedToRoutes(getDefaultLayoutRoutes(), DefaultLayout), ...getLayoutAssignedToRoutes(getErrorLayoutRoutes(), ErrorLayout), { component: FullScreenLayout, @@ -1562,6 +1568,15 @@ export const getAppLayoutRoutes = (): RouteInterface[] => { protected: false, showOnSidePanel: false }, + { + component: DefaultLayout, + icon: null, + id: "default", + name: "Default", + path: AppConstants.getDefaultLayoutBasePath(), + protected: false, + showOnSidePanel: false + }, { component: DashboardLayout, icon: null, diff --git a/apps/console/src/layouts/default-layout.scss b/apps/console/src/layouts/default-layout.scss new file mode 100644 index 00000000000..65ff0857750 --- /dev/null +++ b/apps/console/src/layouts/default-layout.scss @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); 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. + */ + +.default-layout-content { + padding: 2% 5%; +} diff --git a/apps/console/src/layouts/default-layout.tsx b/apps/console/src/layouts/default-layout.tsx index 03ab5e80fc7..847b565c528 100644 --- a/apps/console/src/layouts/default-layout.tsx +++ b/apps/console/src/layouts/default-layout.tsx @@ -16,41 +16,39 @@ * under the License. */ +import AppShell from "@oxygen-ui/react/AppShell"; +import { getProfileInformation } from "@wso2is/admin.authentication.v1/store"; import { AppConstants, AppState, - Footer, + AppUtils, + FeatureConfigInterface, Header, ProtectedRoute, - UIConstants + RouteUtils, + UIConstants, + getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1"; -import { AlertInterface, RouteInterface } from "@wso2is/core/models"; +import { applicationConfig } from "@wso2is/admin.extensions.v1"; +import { AlertInterface, ProfileInfoInterface, RouteInterface } from "@wso2is/core/models"; import { initializeAlertSystem } from "@wso2is/core/store"; -import { - Alert, - ContentLoader, - DefaultLayout as DefaultLayoutSkeleton, - TopLoadingBar, - useUIElementSizes -} from "@wso2is/react-components"; -import React, { - FunctionComponent, - ReactElement, - Suspense, - useEffect, - useState -} from "react"; +import { RouteUtils as CommonRouteUtils, CommonUtils } from "@wso2is/core/utils"; +import { Alert, ContentLoader, EmptyPlaceholder, ErrorBoundary, LinkButton } from "@wso2is/react-components"; +import isEmpty from "lodash-es/isEmpty"; +import React, { FunctionComponent, ReactElement, ReactNode, Suspense, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { System } from "react-notification-system"; import { useDispatch, useSelector } from "react-redux"; -import { StaticContext } from "react-router"; import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom"; -import { Dispatch } from "redux"; +import { Action } from "reduce-reducers"; +import { ThunkDispatch } from "redux-thunk"; import { getDefaultLayoutRoutes } from "../configs/routes"; +import "./default-layout.scss"; /** * Default page layout component Prop types. */ -export interface DefaultLayoutPropsInterface { +export interface DefaultLayoutPropsInterface extends RouteComponentProps { /** * Is layout fluid. */ @@ -64,112 +62,153 @@ export interface DefaultLayoutPropsInterface { * * @returns Dashboard Layout. */ -export const DefaultLayout: FunctionComponent = ( - props: DefaultLayoutPropsInterface -): ReactElement => { - - const { fluid } = props; +export const DefaultLayout: FunctionComponent = ({ + location +}: DefaultLayoutPropsInterface): ReactElement => { + const { t } = useTranslation(); + const dispatch: ThunkDispatch = useDispatch(); - const dispatch: Dispatch = useDispatch(); - const { headerHeight, footerHeight } = useUIElementSizes({ - footerHeight: UIConstants.DEFAULT_FOOTER_HEIGHT, - headerHeight: UIConstants.DEFAULT_HEADER_HEIGHT, - topLoadingBarHeight: UIConstants.AJAX_TOP_LOADING_BAR_HEIGHT + const profileInfo: ProfileInfoInterface = useSelector((state: AppState) => state.profile?.profileInfo); + const alert: AlertInterface = useSelector((state: AppState) => state.global?.alert); + const alertSystem: System = useSelector((state: AppState) => state.global?.alertSystem); + const featureConfig: FeatureConfigInterface = useSelector((state: AppState) => state.config?.ui?.features); + const allowedScopes: string = useSelector((state: AppState) => state?.auth?.allowedScopes); + const appHomePath: string = useSelector((state: AppState) => state.config?.deployment?.appHomePath); + const isMarketingConsentBannerEnabled: boolean = useSelector((state: AppState) => { + return state?.config?.ui?.isMarketingConsentBannerEnabled; }); - const alert: AlertInterface = useSelector((state: AppState) => state.global.alert); - const alertSystem: System = useSelector((state: AppState) => state.global.alertSystem); - const isAJAXTopLoaderVisible: boolean = useSelector((state: AppState) => state.global.isAJAXTopLoaderVisible); + const [ filteredRoutes, setFilteredRoutes ] = useState(getDefaultLayoutRoutes()); + + useEffect(() => { + if (!isEmpty(profileInfo)) { + return; + } + + dispatch(getProfileInformation()); + }, [ dispatch, profileInfo ]); - const [ defaultLayoutRoutes, setDefaultLayoutRoutes ] = useState(getDefaultLayoutRoutes()); + useEffect(() => { + // Allowed scopes is never empty. Wait until it's defined to filter the routes. + if (isEmpty(allowedScopes)) { + return; + } + + const [ routes, _sanitizedRoutes ] = CommonRouteUtils.filterEnabledRoutes( + getDefaultLayoutRoutes(), + featureConfig, + allowedScopes + ); + + // Try to handle any un-expected routing issues. Returns a void if no issues are found. + RouteUtils.gracefullyHandleRouting(routes, AppConstants.getFullScreenViewBasePath(), location.pathname); + + // Filter the routes and get only the enabled routes defined in the app config. + setFilteredRoutes(routes); + }, [ featureConfig, getDefaultLayoutRoutes, allowedScopes ]); /** - * Listen for base name changes and updated the layout routes. + * Conditionally renders a route. If a route has defined a Redirect to + * URL, it will be directed to the specified one. If the route is stated + * as protected, It'll be rendered using the `ProtectedRoute`. + * + * @param route - Route to be rendered. + * @param key - Index of the route. + * @returns Resolved route to be rendered. */ - useEffect(() => { - setDefaultLayoutRoutes(getDefaultLayoutRoutes()); - }, [ AppConstants.getTenantQualifiedAppBasename() ]); + const renderRoute = (route: RouteInterface, key: number): ReactNode => + route.redirectTo ? ( + + ) : route.protected ? ( + + ) : ( + + route.component ? : null + } + key={ key } + exact={ route.exact } + /> + ); - const handleAlertSystemInitialize = (system: any) => { - dispatch(initializeAlertSystem(system)); + /** + * Resolves the set of routes for the react router. + * This function recursively adds any child routes + * defined. + * + * @returns Set of resolved routes. + */ + const resolveRoutes = (): ReactNode[] => { + const resolvedRoutes: ReactNode[] = []; + + const recurse = (routesArr: RouteInterface[]): void => { + routesArr.forEach((route: RouteInterface, key: number) => { + if (route.path) { + resolvedRoutes.push(renderRoute(route, key)); + } + + if (route.children && route.children instanceof Array && route.children.length > 0) { + recurse(route.children); + } + }); + }; + + recurse([ ...filteredRoutes ]); + + return resolvedRoutes; }; return ( - - ) } - topLoadingBar={ ( - - ) } - footerHeight={ footerHeight } - headerHeight={ headerHeight } - desktopContentTopSpacing={ UIConstants.DASHBOARD_LAYOUT_DESKTOP_CONTENT_TOP_SPACING } - header={ ( -
null } - /> - ) } - footer={ ( -